Skip to content
Express.js ex security 5 min read

Express Security Overview

Express gives you a minimal, unopinionated core, which means securing an application is largely your responsibility rather than the framework’s. That freedom is powerful, but it also means a default Express app ships with no security headers, no rate limiting, and no protection against injection or cross-site attacks. This page maps the threats that matter most — drawn from the OWASP Top 10 — to the concrete Express mitigations covered throughout this section, and frames the whole effort as layered defense: no single control is enough, but together they make exploitation expensive and rare.

Defense in depth

Security is not a checkbox you tick once. A robust Express app stacks independent controls so that a failure in one layer is caught by the next. A typical request passes through several defensive middleware before it ever reaches your route handler.

const express = require("express");
const helmet = require("helmet");
const cors = require("cors");
const rateLimit = require("express-rate-limit");

const app = express();

app.use(helmet());                          // secure response headers
app.use(cors({ origin: "https://app.example.com" })); // restrict origins
app.use(express.json({ limit: "100kb" }));  // bound request body size
app.use(rateLimit({ windowMs: 60_000, max: 100 })); // throttle abuse

app.get("/api/health", (req, res) => {
  res.json({ status: "ok" });
});

app.listen(3000);

Each line above addresses a different class of attack. Order matters: security middleware should run before your routes so every request is filtered.

Tip: Treat every value that crosses a trust boundary — query strings, headers, request bodies, cookies, URL params — as hostile until validated. The server must never assume the client behaves.

Mapping threats to Express mitigations

The table below connects the most common web threats to the Express tools and pages that address them. None of these is exotic; they are the everyday hygiene of a production API.

Threat (OWASP)What it isExpress mitigation
Injection (A03)SQL/NoSQL queries built from raw inputParameterized queries, ORM/ODM, input validation
Cross-Site Scripting (XSS)Malicious markup rendered in a victim’s browserOutput encoding, helmet CSP, sanitization
Cross-Site Request Forgery (CSRF)Forged state-changing requests from another siteCSRF tokens, SameSite cookies
Security misconfiguration (A05)Missing headers, verbose errors, defaultshelmet, disable x-powered-by, locked-down CORS
Broken access control (A01)Acting beyond granted permissionsAuth middleware, per-route authorization
Sensitive data exposureSecrets and PII leakingEnv-managed secrets, HTTPS, redacted logs
Denial of serviceFloods and oversized payloadsRate limiting, body-size limits, timeouts

Injection and input validation

Injection happens whenever untrusted input is concatenated into a query, command, or template. The fix is never to “escape harder” by hand — it is to separate code from data using parameterized queries and to validate input shape at the edge.

// VULNERABLE: string concatenation lets input become SQL
db.query(`SELECT * FROM users WHERE email = '${req.body.email}'`);

// SAFE: the driver binds the value, never interpreting it as SQL
db.query("SELECT * FROM users WHERE email = $1", [req.body.email]);

Pair parameterized queries with a schema validator (such as express-validator or Joi) so malformed or unexpected input is rejected with a 400 before it touches your data layer. See SQL & NoSQL injection for the full treatment.

XSS, CSRF, and the browser trust model

Browser-facing threats exploit the trust between a user’s session and your origin. XSS injects script into a page; the defense is to encode all output and lock down what scripts may run via a Content-Security-Policy header. CSRF tricks an authenticated browser into submitting a request the user never intended; the defense is anti-CSRF tokens plus SameSite cookies.

app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      objectSrc: ["'none'"],
    },
  })
);

// HttpOnly + SameSite cookies blunt both XSS token theft and CSRF
res.cookie("sid", sessionId, {
  httpOnly: true,
  secure: true,
  sameSite: "lax",
});

Output:

$ curl -I https://app.example.com/
HTTP/2 200
content-security-policy: default-src 'self'; script-src 'self'; object-src 'none'
x-content-type-options: nosniff
set-cookie: sid=...; HttpOnly; Secure; SameSite=Lax

Express 4.x vs 5.x security notes

Most security middleware (helmet, cors, express-rate-limit) works identically across Express 4 and 5. The most relevant change in Express 5 is its stricter handling of route paths and the removal of some legacy behaviors that could mask bugs. Express 5 also defaults to safer error handling for rejected promises in async route handlers, so an unhandled rejection no longer silently hangs the request.

// Express 5: rejected promises in async handlers reach your error middleware
app.get("/users/:id", async (req, res) => {
  const user = await db.users.find(req.params.id); // may reject
  res.json(user);
});

app.use((err, req, res, next) => {
  console.error(err); // never leak the stack to the client
  res.status(500).json({ error: "Internal Server Error" });
});

Warning: Default Express error pages can echo stack traces and file paths to the client in development. Always register a catch-all error handler that returns a generic message in production — leaked internals are a reconnaissance gift to attackers.

Best Practices

  • Add helmet and a strict Content-Security-Policy on day one — secure headers are the cheapest high-impact control you can apply.
  • Validate and sanitize every input at the boundary, and use parameterized queries so input can never become executable code.
  • Restrict CORS to known origins instead of reflecting *, and prefer HttpOnly, Secure, SameSite cookies for session state.
  • Apply rate limiting and request body-size limits to blunt brute-force and denial-of-service attempts.
  • Keep all secrets in environment variables, never in source control, and serve everything over HTTPS.
  • Run npm audit (and Dependabot/Snyk) regularly — most real-world breaches come through vulnerable dependencies, not your own code.
  • Return generic error messages in production and log details server-side only, so failures never leak internal structure.
Last updated June 14, 2026
Was this helpful?