Reflected cross-site scripting in Express

Reflected cross-site scripting in Express

Description

Reflected XSS occurs when untrusted request parameters, headers or other input are inserted into an HTML response without encoding for the output context. The browser interprets the value as HTML or JavaScript, allowing an attacker to run code in the user's browser. Search results, error messages or profile pages may reflect <script> tags or event handlers that steal accessible credentials, impersonate a user or redirect them.

Potential impact

  • Scripts may steal accessible cookies or tokens and compromise an account.
  • Arbitrary JavaScript may run with the page's permissions.
  • Fake login forms or redirects may support phishing.
  • Altered pages or automatic form submissions may cause unintended actions.
  • Sensitive page data, such as email addresses or CSRF tokens, may be sent to an attacker.

Remediation

  • Encode values for their output location, including HTML text, attributes or URLs. Validate URL schemes and destinations as well. HTML sanitizers are for deliberately allowed markup and differ from text encoding.
  • Use a template engine with automatic escaping, such as Pug, and avoid concatenating untrusted values into HTML.
  • Where practical, return JSON with res.json, set the content type correctly, and display response values through safe text APIs on the client.
  • Validate expected lengths, patterns and allowed characters.
  • Use CSP and X-Content-Type-Options as additional defenses; they do not replace output encoding.
  • Keep Express, template engines and encoding or sanitization libraries updated.

Examples

Before

javascript
const express = require("express");
const app = express();

// Insert user input directly into HTML
app.get("/greet", (req, res) => {
  const name = req.query.name || "guest";
  // A name such as "<img src=x onerror=alert(1)>" can execute a script
  res.type("text/html").send(`<h1>Hello ${name}</h1>`);
});

app.listen(3000);

After

javascript
const express = require("express");
const he = require("he"); // HTML encoding library
const app = express();

// Encode for this output context
app.get("/greet", (req, res) => {
  const rawName = typeof req.query.name === "string" ? req.query.name : "guest";
  const safeName = he.encode(rawName, { useNamedReferences: true }); // Encode &, <, >, ", ' and other characters
  res.type("text/html").send(`<h1>Hello ${safeName}</h1>`);
});

// Or return JSON
app.get("/greet.json", (req, res) => {
  const name = String(req.query.name || "guest");
  res.json({ message: "Hello", name });
});

app.listen(3000);

Explanation:

  • Before: Input is concatenated directly into HTML. Injected tags or event handlers can run malicious JavaScript and enable account abuse or phishing.
  • After: he.encode converts special characters to entities for the HTML body before sending the response. The JSON alternative avoids returning HTML; the client must still render the values safely.

References