Customizing and Disabling Helmet CSP Directives

This guide is part of the Node.js Express Helmet Configuration reference and covers the exact mechanics of reshaping Helmet’s Content-Security-Policy middleware: overriding individual directives, controlling whether your object merges with or replaces Helmet’s baseline via useDefaults, attaching a different policy per route, switching to report-only, and turning the CSP middleware — or any single Helmet middleware — off entirely. Every configuration here is a runnable Express fragment.

Configuration Syntax & Exact Values

Helmet’s CSP middleware is helmet.contentSecurityPolicy(options). It takes three keys that matter for customization: useDefaults, directives, and reportOnly.

helmet.contentSecurityPolicy({
  useDefaults: true,        // merge your directives onto Helmet's baseline
  directives: {
    "script-src": ["'self'", "https://cdn.example.com"],
    "img-src": ["'self'", "data:"],
    "upgrade-insecure-requests": [],   // valueless directive: pass []
  },
  reportOnly: false,        // true => Content-Security-Policy-Report-Only
});

The resulting header string is assembled from directives in insertion order, each directive rendered as name value1 value2; :

Content-Security-Policy: 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' https://cdn.example.com; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; upgrade-insecure-requests

Directive breakdown for the customization keys:

The critical rule is per-directive replacement, not deep merge: if the baseline img-src is 'self' data: and you set imgSrc: ["'self'"], the result is 'self' — your array wins outright, it is not unioned with the baseline values.

How useDefaults controls directive merging A comparison showing Helmet's baseline and a user directives object combining differently under useDefaults true versus false. Helmet baseline default-src 'self' img-src 'self' data: object-src 'none' script-src 'self' Your directives img-src 'self' script-src 'self' cdn object-src: null useDefaults: true default-src 'self' (kept) img-src 'self' (replaced) object-src (removed by null) script-src 'self' cdn (replaced) useDefaults: false img-src 'self' script-src 'self' cdn (nothing else)
Under useDefaults:true your directives replace baseline entries name-by-name; under false only your object survives.

Server-Side Configuration

Overriding directives on the global policy

Register helmet once and pass a customized contentSecurityPolicy. This example keeps Helmet’s baseline and layers on a CDN for scripts plus a data: image source.

const express = require("express");
const helmet = require("helmet");

const app = express();

app.use(
  helmet({
    contentSecurityPolicy: {
      useDefaults: true,
      directives: {
        scriptSrc: ["'self'", "https://cdn.example.com"],
        imgSrc: ["'self'", "data:"],
        connectSrc: ["'self'", "https://api.example.com"],
        objectSrc: null, // drop Helmet's baseline object-src directive
      },
    },
  })
);

useDefaults: true is what lets you specify only the three directives you care about while default-src, base-uri, frame-ancestors, and the rest survive from the baseline. objectSrc: null removes that baseline directive rather than emitting it.

Replacing the policy entirely

For an auditable, self-contained policy, set useDefaults: false and list every directive. Nothing is merged, so the header is exactly what you wrote.

app.use(
  helmet({
    contentSecurityPolicy: {
      useDefaults: false,
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'"],
        styleSrc: ["'self'"],
        imgSrc: ["'self'", "data:"],
        connectSrc: ["'self'"],
        objectSrc: ["'none'"],
        baseUri: ["'none'"],
        frameAncestors: ["'none'"],
        upgradeInsecureRequests: [],
      },
    },
  })
);

Per-route CSP

Mount a stricter or looser policy on specific routes by adding a second CSP middleware after the global one. The last Content-Security-Policy header set for a response wins, so a route-level helmet.contentSecurityPolicy(...) overrides the app-wide policy for that path only.

// App-wide baseline for every response.
app.use(helmet());

// A route that embeds a third-party widget needs a looser script-src.
const widgetCsp = helmet.contentSecurityPolicy({
  useDefaults: true,
  directives: {
    scriptSrc: ["'self'", "https://widget.vendor.com"],
    frameSrc: ["https://widget.vendor.com"],
  },
});

app.get("/embed", widgetCsp, (req, res) => {
  res.render("embed");
});

Because helmet.contentSecurityPolicy calls res.setHeader, the route-level middleware replaces the header value set by the global helmet() for /embed. Every other route keeps the baseline.

Global versus per-route CSP resolution A request flows through the global Helmet policy, then a route-specific CSP middleware overrides the header only for matching routes. Request any path app.use(helmet()) sets baseline CSP route === /embed ? widgetCsp overrides header looser script-src wins baseline header stands all other routes yes no
The route-level CSP middleware runs after the global one and replaces the header for matching paths; every other route keeps the baseline.

Disabling a single Helmet middleware

Pass false for any middleware key in the top-level helmet(...) options to turn just that one off while keeping the rest. This is how you ship Helmet without CSP — for example when a reverse proxy or a separate CDN layer owns the policy.

app.use(
  helmet({
    contentSecurityPolicy: false, // no CSP header emitted by Helmet
    // every other Helmet default (HSTS, X-Content-Type-Options,
    // X-Frame-Options, Referrer-Policy, etc.) still applies
  })
);

Disabling CSP does not touch HSTS, X-Frame-Options, or Referrer-Policy — those middlewares keep their defaults. Only the one key you set to false is removed.

Report-only mode

Set reportOnly: true to evaluate the policy without enforcing it. The header becomes Content-Security-Policy-Report-Only; violations surface in DevTools and are POSTed to any report-to/report-uri endpoint, but nothing is blocked. This is the safe way to trial a tightened policy.

app.use(
  helmet({
    contentSecurityPolicy: {
      useDefaults: true,
      directives: {
        scriptSrc: ["'self'"],
        "report-uri": ["/csp-report"],
      },
      reportOnly: true, // emits Content-Security-Policy-Report-Only
    },
  })
);

Diagnostic & Verification Steps

Confirm the emitted header matches your directives exactly.

# Inspect the enforced CSP header
curl -sI http://localhost:3000/ | grep -i 'content-security-policy'

Expected output for the “replacing the policy entirely” config above:

content-security-policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; upgrade-insecure-requests

Verify report-only mode changed the header name, not just the value:

curl -sI http://localhost:3000/ | grep -i 'content-security-policy-report-only'

Expected output:

content-security-policy-report-only: default-src 'self'; ... ; script-src 'self'; report-uri /csp-report

Confirm a disabled CSP emits no CSP header at all, while other Helmet headers remain:

curl -sI http://localhost:3000/ | grep -iE 'content-security-policy|x-content-type-options|strict-transport-security'

Expected output (no CSP line; other headers present):

strict-transport-security: max-age=15552000; includeSubDomains
x-content-type-options: nosniff

Check that a per-route override actually differs from the baseline:

curl -sI http://localhost:3000/       | grep -i 'content-security-policy'
curl -sI http://localhost:3000/embed  | grep -i 'content-security-policy'

The /embed line must include https://widget.vendor.com in script-src; the / line must not. In browser DevTools, the Network panel’s Response Headers for each request shows the same distinction, and the Console reports any Refused to load violation against the active policy for that page.

Edge Cases, Security Implications & Safe Rollback

Safe rollback. Helmet changes are code-level and non-destructive. To back out a tightened policy without redeploying a full revert:

  1. Add reportOnly: true to the contentSecurityPolicy options — violations are reported, nothing blocks, and the site behaves as before.
  2. Or set contentSecurityPolicy: false to remove the CSP header entirely while every other Helmet header stays in place.
  3. Verify with curl -sI that the header reverts to Content-Security-Policy-Report-Only or disappears, then redeploy the corrected directives.
CSP customization rollout and rollback states A state machine moving from report-only monitoring to enforced policy, with a rollback path back to report-only or disabled. reportOnly: true monitor on staging enforcing production csp: false disabled / proxy owns no reports rollback: violations found hard off
Trial in report-only, promote to enforcing when reports are clean, and fall back to report-only or a disabled middleware if a violation surfaces.

Conclusion

Trial every customized policy in reportOnly: true on staging first, watching DevTools and your report endpoint until legitimate resources stop appearing as violations. Promote the identical directives object to enforcing in production, keeping useDefaults: false for an auditable policy or null overrides when you merge. If a violation slips through, flip back to report-only or set contentSecurityPolicy: false to unblock immediately, fix the directive, and redeploy.

Frequently Asked Questions

Does setting a directive under useDefaults:true add to or replace Helmet’s baseline value? It replaces it. Directive merging happens per name, not per value: your array becomes the entire directive. If you need Helmet’s baseline values plus your own, list both explicitly — for example imgSrc: ["'self'", "data:", "https://cdn.example.com"] — because nothing is unioned automatically.

How do I remove one of Helmet’s default CSP directives without rewriting the whole policy? Keep useDefaults: true and set that directive to null. For example objectSrc: null drops the baseline object-src from the merged header while every other default survives. Setting it to [] instead would emit a valueless directive, which is different.

What is the difference between contentSecurityPolicy:false and reportOnly:true? contentSecurityPolicy: false removes the CSP header entirely — Helmet emits nothing for CSP. reportOnly: true still sends a policy, but as Content-Security-Policy-Report-Only, so violations are reported without being blocked. Use reportOnly to trial a policy and false only when another layer owns the header.

Can different routes have different CSP policies in one Express app? Yes. Register the global helmet() first, then attach a route-specific helmet.contentSecurityPolicy(...) as middleware on the routes that need it. Because the route-level middleware sets the header after the global one, it replaces the value for those paths while all other routes keep the baseline.

If I disable CSP, do I lose the rest of Helmet’s protection? No. Passing contentSecurityPolicy: false turns off only the CSP middleware. HSTS, X-Content-Type-Options: nosniff, X-Frame-Options, Referrer-Policy, and the other defaults keep their values. Each Helmet middleware is toggled independently by its own key.