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:
- A user submits a comment containing JavaScript code:
<script>stealCookies()</script>
- Your application stores this comment in the database
- 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:
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:
<h1>Welcome, <%= user.name %></h1>
Output:
Welcome, John <script>alert('XSS');</script> 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:
npm install dompurify jsdom
Then use it in your Express application:
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:
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:
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:
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:
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
- Context-Aware Sanitization: Different contexts (HTML, JavaScript, CSS, URLs) require different sanitization approaches
- Defense in Depth: Use multiple layers of protection including template escaping, DOMPurify, and CSP headers
- Whitelist, Don't Blacklist: Define what's allowed rather than trying to block what's not
- Sanitize at the Last Possible Moment: Sanitize just before output, not when storing data
- Use Established Libraries: Don't implement your own sanitization logic; use tested libraries
- 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
- OWASP XSS Prevention Cheat Sheet
- DOMPurify Documentation
- Helmet.js Security
- Content Security Policy MDN Documentation
Exercises
- Create a simple Express application that accepts a comment submission form and displays the comments safely.
- Implement DOMPurify in an Express route that needs to render user-submitted HTML content.
- Add appropriate Content Security Policy headers to an existing Express application.
- Create a markdown-based blog that safely renders user-submitted content.
- 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! :)