Blog

Helmet and Express.js

Key Takeaways

  • Helmet is a collection of Express middleware that sets security-related HTTP response headers. Browsers enforce the policies those headers describe.
  • Defaults are a starting point, not an application-specific security policy. Review CSP, HSTS, and cross-origin behavior before deployment.
  • Roll out restrictive CSP changes in report-only mode, then enforce them after testing scripts, styles, embeds, and third-party integrations.
  • Helmet doesn't configure CORS or replace authentication, authorization, output encoding, CSRF protection, or secure cookies.

Helmet improves baseline security in an Express.js application by setting HTTP response headers that control browser behavior. Registering app.use(helmet()) enables its default collection of middleware, but you still need to understand and configure the protections your application requires.

Think of Helmet as browser-policy plumbing, not a vulnerability scanner or a complete security solution. A restrictive script policy can limit what injected code does; it doesn't fix the unsafe rendering that allowed the injection.

Helmet Express Setup

The examples and default inventory below target Helmet 8.3.0, Express 5, and Node.js 24 LTS. Defaults have changed over Helmet's lifetime, so check your installed version when following older tutorials.

In a new project directory, initialize the package and install the dependencies:

npm init --yes
npm install express@5 helmet@8.3.0

Save this as server.mjs. Node treats files ending in .mjs as ES modules, so these imports don't require a package.json change.

import express from "express";
import helmet from "helmet";

const app = express();

app.use(helmet());

app.get("/", (_req, res) => {
  res.type("html").send("<p>Helmet is configured.</p>");
});

app.listen(3000, "127.0.0.1").on("error", (error) => {
  console.error("Server failed to start:", error);
  process.exitCode = 1;
});

Start it with node server.mjs. Register Helmet before routes and static-file middleware that send responses. If middleware order is unfamiliar, review how Express middleware works.

Request -> Helmet -> route handler
             |             |
             adds headers  produces response body
             +-------------+
                    |
                    v
          Browser applies response policies

The Helmet Collection: Current Headers and Defaults

This inventory follows Helmet's header reference. Older names such as frameguard and hsts remain legacy aliases in Helmet 8.3.0; the header-named APIs make new configuration easier to read.

MiddlewareDefault response behaviorPurpose and caveat
contentSecurityPolicyContent-Security-Policy enabled; full policy below.Restricts resource loading and script execution. Requires application-specific review.
crossOriginOpenerPolicyCross-Origin-Opener-Policy: same-originSeparates cross-origin opener relationships. Test popup-based integrations.
crossOriginResourcePolicyCross-Origin-Resource-Policy: same-originRestricts cross-origin resource loading in no-cors mode.
originAgentClusterOrigin-Agent-Cluster: ?1Requests origin-keyed agent clustering, allowing browser resources to be grouped by origin.
referrerPolicyReferrer-Policy: no-referrerOmits referring-page information from the Referer request header.
strictTransportSecurity / hstsStrict-Transport-Security: max-age=31536000; includeSubDomainsEstablishes a browser HTTPS policy. Review subdomains before adopting this default.
xContentTypeOptions / noSniffX-Content-Type-Options: nosniffRestricts MIME-type sniffing: browsers guessing a response's content type. Send correct Content-Type headers too.
xDnsPrefetchControl / dnsPrefetchControlX-DNS-Prefetch-Control: offControls speculative DNS lookups, with privacy and performance trade-offs. Not a general navigation-privacy guarantee.
xDownloadOptions / ieNoOpenX-Download-Options: noopenLegacy Internet Explorer download behavior, not a primary defense for current browsers.
xFrameOptions / frameguardX-Frame-Options: SAMEORIGINLimits framing to the same origin, mitigating clickjacking. Coordinate with CSP's frame-ancestors directive.
xPermittedCrossDomainPoliciesX-Permitted-Cross-Domain-Policies: noneRestricts legacy cross-domain policy-file behavior in clients such as Adobe products.
xPoweredBy / hidePoweredByRemoves X-Powered-By.Minor information reduction, not a meaningful substitute for fixing vulnerabilities.
xXssProtection / xssFilterX-XSS-Protection: 0Disables obsolete browser XSS filters rather than enabling them.
crossOriginEmbedderPolicyCross-Origin-Embedder-Policy is not set.Opt-in control over cross-origin resource embedding; evaluate compatibility before enabling.

You can configure each protection through helmet({ ... }) or disable it with false. Individual middleware also works independently: helmet.referrerPolicy(), for example, doesn't enable the rest of Helmet.

Configure Helmet Content Security Policy for Your Application

Content Security Policy, or CSP, tells the browser which resources and script execution patterns a page permits. It adds a layer of protection against cross-site scripting, or XSS, where attacker-controlled code executes in your site's context.

Helmet's default CSP is the following policy, formatted for readability:

default-src 'self';
base-uri 'self';
font-src 'self' https: data:;
form-action 'self';
frame-ancestors 'self';
img-src 'self' data:;
object-src 'none';
script-src 'self';
script-src-attr 'none';
style-src 'self' https: 'unsafe-inline';
upgrade-insecure-requests

This is a baseline, not an automatically safe policy for every application. For example, it permits inline styles but restricts scripts to the same origin. Helmet merges your configured directives into these defaults unless you specify useDefaults: false.

Prefer Nonces or Hashes Over Broad Script Allowlists

MDN recommends restrictive, nonce- or hash-based script policies rather than relying on broad host allowlists. A nonce is an unpredictable value generated for one response: the policy authorizes that value, and trusted script elements carry it in their nonce attribute.

Use cryptographic randomness, such as Node's randomBytes(), and generate a fresh nonce for every response. For static inline scripts, hashes are another option; changing the script's contents requires updating its policy hash.

The following fragment replaces the original app.use(helmet()) registration and / route in server.mjs. Keep the existing Express and Helmet imports, app creation, and listener; add the crypto import alongside the other imports.

// Fragment: replace the Helmet registration and "/" route in server.mjs.
import { randomBytes } from "node:crypto";

const isProduction = process.env.NODE_ENV === "production";

app.use((_req, res, next) => {
  res.locals.cspNonce = randomBytes(32).toString("base64");
  next();
});

app.use(helmet({
  contentSecurityPolicy: {
    reportOnly: true, // Change to false after validating the policy.
    directives: {
      baseUri: ["'none'"],
      scriptSrc: [
        (_req, res) => `'nonce-${res.locals.cspNonce}'`,
        "'strict-dynamic'",
      ],
      styleSrc: ["'self'"],
      upgradeInsecureRequests: isProduction ? [] : null,
    },
  },
  strictTransportSecurity: isProduction
    ? { maxAge: 300, includeSubDomains: false }
    : false,
}));

app.get("/", (_req, res) => {
  res.set("Cache-Control", "no-store");
  res.type("html").send(`<!doctype html>
<html lang="en">
<head><meta charset="utf-8"><title>CSP demo</title></head>
<body>
  <p id="status">Waiting for the script.</p>
  <script nonce="${res.locals.cspNonce}">
    document.querySelector("#status").textContent = "Script ran.";
  </script>
</body>
</html>`);
});

The script policy authorizes nonced scripts, and 'strict-dynamic' extends that trust to scripts they dynamically load. Review those script loaders carefully: trusted code must not turn attacker-controlled URLs into executable dependencies. The default script-src-attr 'none' remains, so refactor inline event handlers instead of broadly enabling inline JavaScript.

This demo disables caching for its nonce-bearing HTML. Don't reuse a fixed nonce across responses or cache generated HTML independently of its matching CSP header. A hash-based policy is often a better fit for static, cacheable documents.

Test in Report-Only Mode, Then Enforce

reportOnly: true selects Content-Security-Policy-Report-Only: it evaluates the policy without blocking violations. The example is for browser-side testing and doesn't configure a server-side report collector.

For deployment reporting, connect report-to to an endpoint declared in Reporting-Endpoints. MDN's report-only reference explains the setup and the deprecated report-uri fallback still needed for browsers without report-to support.

  1. Exercise login, payments, analytics, fonts, embeds, and other third-party resources in staging.
  2. Review violations and authorize only the resources the application needs.
  3. Set reportOnly: false, then repeat the browser tests with enforcement enabled.

Cross-Origin Headers Are Not CORS

CORS middleware configures which origins may read responses through browser JavaScript. Helmet doesn't configure CORS, and CORS isn't authentication or authorization.

Helmet's cross-origin headers solve different problems. COOP controls relationships between windows; its default can disrupt popup integrations that need opener access. For those flows, evaluate crossOriginOpenerPolicy: { policy: "same-origin-allow-popups" }, as described in the COOP reference.

CORP controls loading of the protected response in no-cors mode. If your Express app serves public assets intended for other origins, review CORP on those routes rather than disabling all of Helmet to fix a broken image or script.

HSTS and Express Behind a Reverse Proxy

HTTP Strict Transport Security, or HSTS, tells supporting browsers to use HTTPS for future requests to a host for the policy's max-age. Browsers learn it from an HTTPS response; it doesn't secure an initial HTTP visit before the policy is known, unless the host is preloaded.

Configure TLS and HTTP-to-HTTPS redirection separately, commonly at a reverse proxy or load balancer. Helmet doesn't provision certificates, configure TLS termination, or make your HTTP listener reject requests.

The fragment above deliberately starts production HSTS at 300 seconds without subdomains. Increase the duration after validating HTTPS, include subdomains only when they're ready, and treat preloading as a separate domain-wide commitment. Helmet's preload option defaults to false; setting it to true adds a directive, not automatic enrollment in browser preload lists.

Helmet doesn't need Express's trust proxy setting merely to emit HSTS. Configure it when application behavior depends on forwarded connection information, such as req.protocol, and match the actual proxy topology:

// Fragment: only when the trusted proxy connects from loopback.
app.set("trust proxy", "loopback");

Follow Express's reverse-proxy guidance: restrict backend access and ensure trusted proxies overwrite forwarded headers. Don't blindly set trust proxy to true and accept client-supplied forwarding information.

Retire Obsolete Protections and Set Cache Policy Separately

Do Not Deploy HPKP or Expect-CT

HTTP Public Key Pinning, or HPKP, used Public-Key-Pins to pin certificate keys. It is deprecated and no longer part of Helmet. Bad or hostile pins could make a site unavailable, a risk documented in Chrome's HPKP deprecation notice.

That notice's historical recommendation to use Expect-CT is also outdated: Expect-CT is obsolete and shouldn't be deployed. Use current certificate-authority practices and browser enforcement of Certificate Transparency, not a replacement pinning header. Use HSTS for HTTPS enforcement; it serves a different purpose.

noCache Is Not Current Helmet Middleware

helmet.noCache() was removed, as recorded in the Helmet changelog. Choose cache behavior per response instead: sensitive responses may need no-store, while versioned static assets should retain useful caching.

Set the appropriate Cache-Control header through Express or your serving layer. no-store requests that caches not store the response; no-cache instead permits storage but requires revalidation before reuse. Blanket no-store isn't a general security upgrade.

Verify the Headers Your Users Actually Receive

Inspect responses in browser developer tools and use curl for a quick header check. curl -I sends a HEAD request; the second deployed check below uses GET and discards the body.

# Local example:
curl -I http://127.0.0.1:3000/

# Set APP_URL to your deployed HTTPS endpoint first:
curl -I "$APP_URL"

# GET response headers; POSIX shell:
curl -sS -D - -o /dev/null "$APP_URL"

Check the public endpoint, not just the Node process: proxies and CDNs can change headers. Test successful pages, redirects, static assets, and error responses. Keep Helmet ahead of response-producing handlers, including your Express error-handling middleware.

Production Checklist

  • Review browser behavior, not just header presence: test framing, popup login, cross-origin assets, and CSP enforcement.
  • Document ownership of each header. Manual headers or proxy configuration can replace Helmet, but avoid conflicting policies across layers.
  • Keep input validation, context-aware output encoding, safe templating, authentication, authorization, CSRF protection, dependency updates, and secure cookie settings in your security plan.

Use the Express production security guidance and our broader Express application security checklist alongside Helmet. Response headers complement application security; they don't replace it.

FAQ

How do I install and use Helmet in an Express app?

Install Helmet with npm install helmet, import it, and register app.use(helmet()) before your routes and static-file middleware. Review CSP and HSTS before deploying the defaults.

What security headers does Helmet set by default?

For Helmet 8.3.0, defaults include CSP, HSTS, COOP, CORP, Origin-Agent-Cluster, and several X-* headers; Helmet also removes X-Powered-By. Cross-Origin-Embedder-Policy is not enabled by default.

Does Helmet prevent XSS attacks?

Helmet can reduce XSS risk through CSP, but it doesn't remove the underlying vulnerability. Keep context-aware output encoding, safe templating, and appropriate HTML sanitization as primary defenses.

How do I configure Content Security Policy with Helmet?

Configure the contentSecurityPolicy.directives option and initially test with reportOnly: true. For nonce-based script policies, generate a fresh unpredictable nonce for every response and put the same value in the policy and trusted script tags before enforcing it.

Does Helmet configure CORS?

No. Configure CORS separately, for example with Express's cors middleware; Helmet's COOP, CORP, and COEP options address different browser policies. CORS does not replace authentication or authorization.

Further Reading

Written by Sam Erickson

Software engineer. I write every article on StackChief myself. Author of Mastering the Tech Interview. More about me