Express.js HTML Construction XSS

High Risk Cross-site Scripting
xssexpressjavascripthtml-constructiontemplate-literals

What it is

Cross-site scripting (XSS) vulnerability in Express.js applications where HTML is manually constructed using template literals or string concatenation with unescaped user input.

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

app.use(express.urlencoded({ extended: true }));

// Vulnerable: Raw HTML construction
app.get('/welcome', (req, res) => {
    const name = req.query.name || 'Guest';
    const message = req.query.message || 'Welcome!';

    // XSS vulnerability - no encoding
    const html = `
        <html>
        <head><title>Welcome Page</title></head>
        <body>
            <h1>Hello \${name}!</h1>
            <p>\${message}</p>
            <div>Current time: \${new Date()}</div>
        </body>
        </html>
    `;

    res.send(html);
});

// Vulnerable: Manual HTML construction
app.post('/comment', (req, res) => {
    const comment = req.body.comment;
    const author = req.body.author;

    // Direct string concatenation - XSS risk
    const html = '<div class="comment">' +
                 '<h3>' + author + '</h3>' +
                 '<p>' + comment + '</p>' +
                 '</div>';

    res.send(html);
});

// Attack: /?name=<script>alert('XSS')</script>
// Or: comment=<img src=x onerror="document.location='//evil.com'">
const express = require('express');
const escapeHtml = require('escape-html');
const app = express();

app.use(express.urlencoded({ extended: true }));
app.set('view engine', 'ejs'); // Template engine with auto-escaping

// Fixed: Using template engine (views/welcome.ejs)
/*
<html>
<head><title>Welcome Page</title></head>
<body>
    <h1>Hello <%= name %>!</h1>    <!-- Auto-escaped -->
    <p><%- message %></p>           <!-- Raw HTML (use carefully) -->
    <div>Current time: <%= currentTime %></div>
</body>
</html>
*/

app.get('/welcome', (req, res) => {
    const name = req.query.name || 'Guest';
    const message = escapeHtml(req.query.message || 'Welcome!');

    // Safe: Using template with auto-escaping
    res.render('welcome', {
        name: name,        // Auto-escaped by EJS
        message: message,  // Pre-escaped for <%- %>
        currentTime: new Date()
    });
});

// Fixed: Manual encoding approach
app.post('/comment', (req, res) => {
    const comment = req.body.comment;
    const author = req.body.author;

    // Safe: HTML encoding before insertion
    const html = '<div class="comment">' +
                 '<h3>' + escapeHtml(author) + '</h3>' +
                 '<p>' + escapeHtml(comment) + '</p>' +
                 '</div>';

    res.send(html);
});

// Alternative: JSON API approach
app.get('/api/welcome', (req, res) => {
    res.json({
        name: req.query.name || 'Guest',
        message: req.query.message || 'Welcome!',
        timestamp: new Date().toISOString()
    });
});

💡 Why This Fix Works

The vulnerable code constructs HTML manually with unescaped user input. The fixed version uses template engines with auto-escaping or manual HTML encoding functions.

Why it happens

Using template literals to build HTML responses with user input without proper escaping creates XSS vulnerabilities.

Root causes

Template Literal HTML Construction

Using template literals to build HTML responses with user input without proper escaping creates XSS vulnerabilities.

Preview example – JAVASCRIPT
// Vulnerable
const html = `<h1>Hello \${name}!</h1><p>\${message}</p>`;
res.send(html);

String Concatenation HTML Building

Building HTML responses using string concatenation with user input without encoding allows script injection.

Preview example – JAVASCRIPT
// Vulnerable
const html = '<div class="comment">' + '<h3>' + author + '</h3>' + '<p>' + comment + '</p>' + '</div>';
res.send(html);

Fixes

1

Use Template Engines with Auto-Escaping

Use template engines like EJS, Handlebars, or Pug that provide automatic HTML escaping for user input.

View implementation – JAVASCRIPT
app.set('view engine', 'ejs');

// Template: <h1>Hello <%= name %>!</h1>
app.get('/welcome', (req, res) => {
  res.render('welcome', { name: req.query.name });
});
2

Manual HTML Encoding with escape-html

When manual HTML construction is necessary, use libraries like escape-html to encode user input.

View implementation – JAVASCRIPT
const escapeHtml = require('escape-html');

// Safe: HTML encoding before insertion
const html = `<h1>Hello \${escapeHtml(name)}!</h1>`;
res.send(html);

Detect This Vulnerability in Your Code

Sourcery automatically identifies express.js html construction xss and many other security issues in your codebase.