Skip to content
MDView as Markdown

Express

Express has no opinion about security headers, so almost everyone reaches for Helmet, which sets a stack of headers including a default CSP. That default is deliberately strict, and it is usually the first thing that breaks an existing app - which is exactly why you want report-only first.

What Helmet gives you

  • A default Content-Security-Policy applied as soon as you app.use(helmet()), including default-src 'self', script-src 'self', object-src 'none' and upgrade-insecure-requests.
  • Custom directives passed as a nested directives object.
  • Directive values that can be functions receiving the request and response, which is how per-request nonces get in.
  • A reportOnly flag that swaps the header for Content-Security-Policy-Report-Only.

Setting the header

Your URLs come from the Setup page:

app.use(
  helmet({
    contentSecurityPolicy: {
      reportOnly: true,
      directives: {
        "default-src": ["'self'"],
        "script-src": ["'self'", "'report-sha256'"],
        "report-uri": ["https://abc123.report-uri.com/r/d/csp/reportOnly"],
        "report-to": ["default"],
      },
    },
  }),
);
app.use((req, res, next) => {
  res.setHeader("Reporting-Endpoints", 'default="https://abc123.report-uri.com/a/d/g"');
  res.setHeader("Report-To", JSON.stringify({
    group: "default",
    max_age: 31536000,
    endpoints: [{ url: "https://abc123.report-uri.com/a/d/g" }],
    include_subdomains: true,
  }));
  next();
});

Helmet does not set the two Reporting API headers, so the second middleware does. The Reporting-Endpoints and Report-To headers tell the browser where report-to default sends reports, including the CSP Integrity reports that 'report-sha256' turns on.

Adding a nonce

Generate the nonce before Helmet runs, then read it back in a directive function:

import crypto from 'node:crypto'
app.use((req, res, next) => {
  res.locals.nonce = crypto.randomBytes(16).toString('base64')
  next()
})
app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        "script-src": ["'self'", "'report-sha256'", (req, res) => `'nonce-${res.locals.nonce}'`],
      },
    },
  }),
)

Then render nonce="<%= nonce %>" on your inline scripts.

Things to watch for

  • Custom directives replace Helmet's defaults for the directives you name, and Helmet merges the rest. Check the header you actually emit rather than assuming your object is additive.
  • Middleware order matters: the nonce middleware has to run before Helmet, or the directive function reads undefined.
  • Helmet's defaults change between major versions. Pin the version and re-check the emitted header after an upgrade, rather than discovering it from production reports.

Official documentation

Helmet - Content Security Policy

Start Monitoring with Report URI

Already using us for CSP? Find your CSP reporting directives on Setup, or view your CSP reports.

New to Report URI? Create an account and grab your CSP reporting directives from the Setup page.

Start your free trial