Prevent XSS Attacks in Node.js

Cross-Site Scripting (XSS) remains one of the most prevalent security vulnerabilities in web applications, allowing attackers to inject malicious scripts into webpages viewed by other users. This article provides a direct, actionable guide on how to secure your Node.js applications against XSS attacks. You will learn the essential security practices to implement immediately, including input sanitization, output encoding, securing HTTP headers with Helmet, and protecting session cookies.

1. Sanitize and Validate User Input

Never trust user input. Attackers can submit malicious scripts through form fields, query parameters, or JSON payloads. To block these attempts, you must validate and sanitize all incoming data.

Use the express-validator library to validate that inputs match expected formats (e.g., ensuring an email field actually contains an email) and to strip out potentially dangerous characters.

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

app.post('/submit', [
  body('username').trim().escape(),
  body('email').isEmail().normalizeEmail()
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({ errors: errors.array() });
}
  // Process safe data
});

If your application must accept rich text (like HTML from a WYSIWYG editor), use a dedicated library like sanitize-html to whitelist only safe HTML tags and attributes, discarding any <script> or onload attributes.

const sanitizeHtml = require('sanitize-html');

const cleanHTML = sanitizeHtml(dirtyHTML, {
  allowedTags: [ 'b', 'i', 'em', 'strong', 'a' ],
  allowedAttributes: {
    'a': [ 'href' ]
  }
});

2. Use Context-Aware Output Encoding

XSS occurs when the browser interprets user-supplied data as executable code. Output encoding converts special characters into their safe HTML entity equivalents (e.g., converting < to &lt; and " to &quot;).

If you are using modern frontend frameworks like React, Angular, or Vue, they perform automatic HTML escaping by default. If you are rendering templates on the server side using engines like EJS, Pug, or Handlebars, ensure you use the correct escaping syntax:

3. Implement a Content Security Policy (CSP)

A Content Security Policy (CSP) is an HTTP header that restricts the resources (such as JavaScript, CSS, and Images) that the browser is allowed to load for a given page. A strong CSP can completely neutralize XSS attacks by preventing unauthorized inline scripts from executing.

The easiest way to implement CSP in a Node.js/Express application is by using the helmet middleware.

const express = require('express');
const helmet = require('helmet');

const app = express();

// Use Helmet to set secure HTTP headers, including CSP
app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", "https://trustedscripts.com"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:"],
      },
    },
  })
);

4. Secure Session Cookies

Attackers often exploit XSS vulnerabilities to steal session tokens stored in cookies, allowing them to hijack user accounts. You can prevent JavaScript from reading your cookies by setting the HttpOnly flag.

When configuring your session middleware (like express-session), always apply the following security flags:

const session = require('express-session');

app.use(session({
  secret: 'your-secure-secret',
  resave: false,
  saveUninitialized: true,
  cookie: {
    httpOnly: true,
    secure: true, // Requires HTTPS
    sameSite: 'strict'
  }
}));