pwnmyvibecode_

Security headers for Express

Install Helmet and call app.use(helmet()) before your routes; it sets HSTS, a strict CSP, frame protection, nosniff and a Referrer-Policy, and removes X-Powered-By. If you'd rather not add a dependency, a small middleware that calls res.setHeader for each header does the same job.

Updated

Check your live site now

Free, no signup, read only. A grade and plain fixes in seconds.

What Express sends by default

A fresh Express app sends X-Powered-By: Express on every response and no security headers at all. The headers worth adding are HSTS (always use https), a Content-Security-Policy (limits where scripts can load from), X-Frame-Options (stops clickjacking), X-Content-Type-Options: nosniff (stops file type guessing), Referrer-Policy (limits URL leakage to other sites) and Permissions-Policy (switches off browser features you don't use).

Setting security headers with Helmet

terminal
npm install helmet
server.js
import express from "express";
import helmet from "helmet";

const app = express();
app.use(helmet());

// routes and express.static come after this line

Helmet's defaults include Strict-Transport-Security with includeSubDomains, X-Frame-Options: SAMEORIGIN, X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, and a Content-Security-Policy whose script-src is 'self'. It also removes X-Powered-By. It doesn't set Permissions-Policy, so add that one yourself.

The default CSP is strict about scripts, which is good, and it will block any inline script tags your pages render. If a page stops working after adding Helmet, open the browser console: blocked scripts are listed there, and the next section shows how to allow your own inline scripts safely.

Security headers in Express without Helmet

The same result with plain middleware. Register it before express.static and your routes, because Express runs middleware in the order you add it, and a route that responds first never reaches it.

server.js
app.use((req, res, next) => {
  res.setHeader("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
  res.setHeader("Content-Security-Policy", "default-src 'self' https: data: blob: 'unsafe-inline' 'unsafe-eval'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'; upgrade-insecure-requests");
  res.setHeader("X-Frame-Options", "SAMEORIGIN");
  res.setHeader("X-Content-Type-Options", "nosniff");
  res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
  res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
  next();
});

Add app.disable("x-powered-by") alongside it. The CSP here is a permissive starter that won't break an existing app, so a header checker will still report it as weak. Treat it as a first step toward the nonce setup below.

Changing Helmet's default values

Each header Helmet sets has its own option. Pass an object to helmet() to change a value, or set an option to false to switch that header off. Some common changes:

server.js
app.use(
  helmet({
    // a longer HSTS, two years
    strictTransportSecurity: { maxAge: 63072000, includeSubDomains: true },
    // never allow framing, not even by your own pages
    xFrameOptions: { action: "deny" },
    // send your domain (not the full URL) to other sites
    referrerPolicy: { policy: "strict-origin-when-cross-origin" },
  }),
);

Helmet also sends Cross-Origin-Resource-Policy: same-origin, which stops other origins loading your files. That is a sensible default, but if a front end on a different domain loads images or fonts from this Express server, those requests will be blocked. Set crossOriginResourcePolicy: { policy: "cross-origin" } in that case, rather than removing Helmet.

A nonce-based CSP in Express

To allow your own inline scripts without allowing everyone's, generate a random nonce per request, put it in the CSP, and print it on your script tags. Helmet accepts a function as a directive value for exactly this:

server.js
import crypto from "node:crypto";

app.use((req, res, next) => {
  res.locals.cspNonce = crypto.randomBytes(16).toString("base64");
  next();
});

app.use(
  helmet({
    contentSecurityPolicy: {
      reportOnly: true, // remove once the browser console is clean
      directives: {
        "script-src": ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`],
      },
    },
  }),
);

In your templates, render the nonce on each inline script, for example <script nonce="<%= cspNonce %>"> in EJS. Scripts without the matching nonce are refused. Leave reportOnly on while you click through the app, fix whatever the console reports, then remove it to enforce the policy.

Express security header gotchas

  • Behind a proxy or a platform like Render, Railway or Heroku, set app.set("trust proxy", 1). Without it req.secure is false, https redirects can loop, and express-session won't set a cookie marked secure.
  • Session cookies need their own flags: cookie: { secure: true, httpOnly: true, sameSite: "lax" } in express-session. Headers don't fix cookie settings.
  • If nginx or your platform also adds security headers, visitors can receive two copies with different values. Decide which layer owns each header.
  • A JSON-only API still benefits from nosniff and HSTS. The CSP and frame headers matter most on responses that return HTML.

How to confirm the Express headers

terminal
curl -sI http://localhost:3000 | grep -i -E 'content-security|x-frame|x-content-type|referrer-policy|permissions-policy|x-powered-by'
curl -sI https://your-app.com | grep -i strict-transport

Check locally first, then on the deployed URL, since HSTS only means something over https and some platforms rewrite headers. There should be no x-powered-by line.

Questions

Is Helmet enough to secure an Express app?

Helmet covers response headers only. You still need input validation, auth checks on every route, safe cookie flags, a strict CORS allowlist and secrets kept out of client code.

Why did my inline scripts stop working after adding Helmet?

Helmet's default CSP allows scripts only from your own domain as files. Inline scripts are blocked until you move them into files or add a per-request nonce as shown above.

Does Helmet set Permissions-Policy?

No. Set it yourself with res.setHeader("Permissions-Policy", "camera=(), microphone=(), geolocation=()") in a small middleware, adjusting the list to the features your app really uses.