Skip to main content

Express Output Sanitization

Introduction

When building web applications with Express, ensuring that the data you send to clients is safe and secure is just as important as validating incoming data. Output sanitization refers to the practice of cleaning and formatting data before sending it to the client to prevent security vulnerabilities such as Cross-Site Scripting (XSS) attacks.

In this lesson, you'll learn why output sanitization matters, common techniques to sanitize output in Express applications, and how to implement these techniques in real-world scenarios.

Why Output Sanitization Matters

Without proper output sanitization, your Express application may inadvertently send malicious code to users' browsers. Consider this scenario:

  1. A user submits a comment containing JavaScript code: <script>stealCookies()</script>
  2. Your application stores this comment in the database
  3. When another user views the comment, if it's not sanitized, their browser will execute the script, potentially compromising their data

This is a classic example of a Cross-Site Scripting (XSS) attack, which can lead to:

  • Cookie theft and session hijacking
  • Keylogging and data theft
  • Defacement of web pages
  • Redirection to malicious websites

Methods for Output Sanitization in Express

1. Using Template Engines with Built-in Escaping

Most template engines used with Express provide automatic HTML escaping. For example, if you're using EJS:

javascript
app.set('view engine', 'ejs');

app.get('/profile', (req, res) => {
const userData = {
name: "John <script>alert('XSS');</script> Doe"
};

res.render('profile', { user: userData });
});

In your EJS template, using <%= %> automatically escapes HTML:

html
<h1>Welcome, <%= user.name %></h1>

Output:

Welcome, John &lt;script&gt;alert('XSS');&lt;/script&gt; Doe

2. Using DOMPurify for HTML Content

When you need to allow some HTML but want to strip unsafe elements, DOMPurify is an excellent library:

First, install it:

bash
npm install dompurify jsdom

Then use it in your Express application:

javascript
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');

const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);

app.get('/article/:id', async (req, res) => {
// Fetch article from database
const article = await Article.findById(req.params.id);

// Sanitize HTML content
const cleanHtml = DOMPurify.sanitize(article.content);

res.render('article', {
title: article.title,
content: cleanHtml
});
});

3. Using express-validator for API Responses

While express-validator is commonly used for input validation, it can also help with sanitizing data for API responses:

javascript
const { validationResult } = require('express-validator');

app.get('/api/users', async (req, res) => {
try {
const users = await User.find({});

// Sanitize user data before sending
const sanitizedUsers = users.map(user => ({
id: user._id,
name: user.name,
email: user.email
// Only include necessary fields, exclude sensitive data
}));

res.json({ success: true, users: sanitizedUsers });
} catch (error) {
res.status(500).json({ success: false, error: 'Server error' });
}
});

4. Content-Security-Policy Headers

Adding Content Security Policy headers is another layer of defense:

javascript
const helmet = require('helmet');

// Add Helmet's CSP middleware
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "trusted-cdn.com"],
styleSrc: ["'self'", "trusted-cdn.com"],
imgSrc: ["'self'", "data:", "trusted-cdn.com"],
connectSrc: ["'self'"],
fontSrc: ["'self'", "trusted-cdn.com"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"]
}
}));

Real-World Examples

Markdown Blog with Safe Rendering

For a blog that accepts markdown content but needs to display it safely:

javascript
const marked = require('marked');
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');

const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);

// Configure marked to be more secure
marked.setOptions({
sanitize: true
});

app.get('/blog/:slug', async (req, res) => {
const post = await BlogPost.findOne({ slug: req.params.slug });

if (!post) {
return res.status(404).render('404');
}

// Convert markdown to HTML
let htmlContent = marked.parse(post.content);

// Additional sanitization with DOMPurify
htmlContent = DOMPurify.sanitize(htmlContent, {
ALLOWED_TAGS: ['h1', 'h2', 'h3', 'p', 'a', 'ul', 'ol', 'li', 'strong', 'em', 'code', 'pre'],
ALLOWED_ATTR: ['href', 'target', 'rel']
});

res.render('blog-post', {
title: DOMPurify.sanitize(post.title),
content: htmlContent,
author: DOMPurify.sanitize(post.author.name),
date: new Date(post.createdAt).toLocaleDateString()
});
});

User Profile Page with Safe Rendering

For a user profile page that displays user-generated content:

javascript
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const { escape } = require('html-escaper');

const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);

app.get('/profile/:username', async (req, res) => {
try {
const user = await User.findOne({ username: req.params.username });

if (!user) {
return res.status(404).render('404');
}

// Sanitize different types of content appropriately
const safeProfile = {
username: escape(user.username),
displayName: escape(user.displayName),
// Allow limited HTML in bio
bio: user.bio ? DOMPurify.sanitize(user.bio, {
ALLOWED_TAGS: ['p', 'a', 'em', 'strong'],
ALLOWED_ATTR: ['href']
}) : '',
// No HTML allowed in social links
socialLinks: {
twitter: user.socialLinks?.twitter ? escape(user.socialLinks.twitter) : null,
github: user.socialLinks?.github ? escape(user.socialLinks.github) : null,
linkedin: user.socialLinks?.linkedin ? escape(user.socialLinks.linkedin) : null
},
// Prevent HTML in skills list
skills: user.skills.map(skill => escape(skill))
};

res.render('profile', { user: safeProfile });
} catch (error) {
console.error('Profile error:', error);
res.status(500).render('error', { message: 'Server error' });
}
});

Best Practices for Output Sanitization

  1. Context-Aware Sanitization: Different contexts (HTML, JavaScript, CSS, URLs) require different sanitization approaches
  2. Defense in Depth: Use multiple layers of protection including template escaping, DOMPurify, and CSP headers
  3. Whitelist, Don't Blacklist: Define what's allowed rather than trying to block what's not
  4. Sanitize at the Last Possible Moment: Sanitize just before output, not when storing data
  5. Use Established Libraries: Don't implement your own sanitization logic; use tested libraries
  6. Keep Dependencies Updated: Security vulnerabilities are regularly found and patched in libraries

Common Pitfalls to Avoid

  • Using innerHTML in Client-Side Code: If you must use it, sanitize content first
  • Bypassing Template Engine Escaping: Avoid using syntax that bypasses automatic escaping (like {{{ in Handlebars or <%- %> in EJS)
  • Trusting Content From Authenticated Users: Even content from logged-in users should be sanitized
  • Forgetting About URL Contexts: Sanitizing URLs requires different approaches than HTML content
  • Not Sanitizing JSON Responses: API responses need sanitization too

Summary

Output sanitization is a critical aspect of web application security that helps prevent XSS attacks and other vulnerabilities. By properly sanitizing data before presenting it to users, you ensure that your Express application doesn't become a vector for attacks.

In this lesson, we've covered:

  • Why output sanitization matters in Express applications
  • Various methods for sanitizing output including template engines, DOMPurify, and express-validator
  • Real-world examples showing how to implement sanitization
  • Best practices and common pitfalls to avoid

Remember that security is a layered process, and output sanitization is just one important component of a comprehensive security strategy.

Additional Resources

Exercises

  1. Create a simple Express application that accepts a comment submission form and displays the comments safely.
  2. Implement DOMPurify in an Express route that needs to render user-submitted HTML content.
  3. Add appropriate Content Security Policy headers to an existing Express application.
  4. Create a markdown-based blog that safely renders user-submitted content.
  5. Modify an API endpoint to ensure all user data is properly sanitized before being sent in a JSON response.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)