Only 7% of the Alexa top 1 million websites have a valid Content Security Policy (CSP), and just 2% have implemented what security researchers call a “perfect” one, according to a 2025 Blue Triangle analysis. For Node.js developers shipping production apps today, that gap represents both a real exposure and a clear competitive advantage. CSP is one of the few browser-enforced mechanisms that stops cross-site scripting (XSS) attacks at the network layer before injected code can execute, and enabling it in Express takes less than 30 minutes.

This tutorial walks through 12 concrete steps to add a production-grade Content Security Policy to a Node.js application. It covers the built-in middleware from Helmet.js 8.2.0, request-scoped nonce generation, hash-based allowlisting for static scripts, CSP violation reporting, and the correct order to deploy so you never break a live site.

What Is Content Security Policy and Why Node.js Apps Need It

Content Security Policy is an HTTP response header that instructs the browser about which sources of content, scripts, styles, images, and other resources it is allowed to load on a given page. If a script tries to load from a source the policy does not permit, the browser silently blocks it before it can touch the DOM. An attacker who somehow injects a <script src="https://evil.example/payload.js"> into your HTML gets nothing from the user’s browser because the browser sees no matching CSP rule and drops the request entirely.

CSP’s primary use case is XSS mitigation. Cross-site scripting attacks remain one of the most common vulnerabilities in web applications, and they work by injecting malicious JavaScript into a page that runs in the context of the trusted origin. A properly configured CSP makes that class of attack substantially harder or outright impossible in most scenarios. The policy also helps against clickjacking via the frame-ancestors directive, and it can force all resource loads to HTTPS via upgrade-insecure-requests.

The challenge is configuration complexity. Only 21.53% of websites could implement the stronger nonce- or hash-based CSP levels that give the strongest XSS guarantees, according to a 2025 IEEE Computer Society study. About 40% of the sites that could not adopt those stricter modes found implementation genuinely difficult. This tutorial is written specifically to bring those numbers up: by the end, you will have a strict, nonce-based CSP running in Node.js with a validation step to prove it.

Prerequisites

Before starting, confirm these are in place:

  • Node.js 24.x LTS (“Krypton”) or Node.js 22.x LTS minimum. The tutorial uses node:crypto built-ins available since Node.js 14.x, but v24 LTS is the current supported release as of June 2026.
  • npm 10.x or higher, bundled with Node.js 24.
  • Express 5.2.1. The examples work on Express 4.x as well; Express 5 removed some deprecated patterns but the middleware API is compatible.
  • Helmet.js 8.2.0. Helmet sets the Content-Security-Policy header along with 14 other security headers in a single middleware call. Version 8.x introduced stricter defaults, including script-src-attr 'none' to block all inline event handlers by default.
  • A basic understanding of HTTP headers and how Express middleware chains work.
  • A terminal with a package manager and a text editor.

No database or external services are required for this tutorial. The final project runs entirely in memory.

CSP Directives Quick Reference

CSP works through a set of named directives, each controlling a different category of content. The table below covers the directives you will encounter in this tutorial.

DirectiveControlsCommon values
default-srcFallback for all resource types not explicitly set'self', 'none'
script-srcJavaScript <script> elements'self', 'nonce-...', 'sha256-...', 'strict-dynamic'
script-src-attrInline event handlers (onclick=, onerror=)'none' (recommended)
script-src-elem<script> element src specifically (CSP Level 3)'self', 'nonce-...'
style-srcStylesheets and inline styles'self', 'unsafe-inline', 'nonce-...'
img-srcImages'self', data:, CDN host
connect-srcFetch, XHR, WebSocket connections'self', API host
font-srcWeb fonts'self', https:
frame-ancestorsWho can embed this page in a frame'none', 'self'
object-srcPlugins (Flash, Java applets)'none' (always)
base-uriAllowed values for <base href>'self'
form-actionWhere forms can submit'self'
upgrade-insecure-requestsForces HTTP requests to HTTPSNo value needed
report-toEndpoint group for violation reports (CSP Level 3)Group name string

CSP Level 2 established the core directive model. CSP Level 3 added script-src-attr, script-src-elem, 'strict-dynamic', and improvements to reporting through the Reporting API. Helmet 8.x defaults already reflect Level 3 best practices where browser support permits.

Steps 1-3: Set Up the Node.js Project

Step 1: Initialize the Project

Create a fresh directory and initialize npm:

mkdir csp-demo && cd csp-demo
npm init -y

Open package.json and add "type": "module" if you want ES module syntax. The tutorial uses CommonJS (require) for maximum compatibility with existing Express tutorials, so leave the type field at its default.

Step 2: Install Dependencies

npm install express@5 helmet@8

This installs Express 5.2.1 and Helmet 8.2.0. No additional CSP-specific packages are needed. Helmet bundles its own CSP module internally and exposes it through helmet.contentSecurityPolicy().

Step 3: Create a Minimal Express Server

// server.js
const express = require('express');
const app = express();

app.get('/', (req, res) => {
  res.send(`
    
    
      CSP Demo
      
        

CSP Demo

`); }); app.listen(3000, () => console.log('Server running on http://localhost:3000'));

Run node server.js and open http://localhost:3000 in a browser. Check the Network tab in DevTools. You will see no Content-Security-Policy header in the response, which means the inline script runs freely. This is the before state. The next steps add and progressively tighten the policy.

Steps 4-5: Enable Helmet.js and Inspect Default CSP

Step 4: Add Helmet with Default CSP

Adding Helmet with its defaults takes two lines:

const express = require('express');
const helmet = require('helmet');
const app = express();

app.use(helmet()); // includes contentSecurityPolicy by default in Helmet 8.x

app.get('/', (req, res) => {
  res.send(`
    
    
      CSP Demo
      
        

CSP Demo

`); }); app.listen(3000, () => console.log('Server running on http://localhost:3000'));

Restart the server. Reload the page. The browser DevTools console now shows a CSP violation: the inline <script> block was blocked. The Content-Security-Policy header in the response reads:

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

Notice script-src-attr 'none': this is a CSP Level 3 directive that blocks all inline event handlers (onclick, onerror, onload, etc.) globally. Helmet 8.x added this to the defaults because inline event handlers are a major XSS vector that a general script-src rule does not cover on its own.

Step 5: Retrieve and Log Default Directives Programmatically

Helmet exposes its default directive map so you can build on it rather than rewrite it from scratch:

const { contentSecurityPolicy } = require('helmet');

const defaults = contentSecurityPolicy.getDefaultDirectives();
console.log(JSON.stringify(defaults, null, 2));
// Output shows each directive key with its value array
// Example: "scriptSrc": ["'self'"]

This pattern is important when you want to extend defaults without inadvertently removing hardened settings. Instead of writing a complete directive list from scratch, you spread the defaults and override specific keys:

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        ...contentSecurityPolicy.getDefaultDirectives(),
        imgSrc: ["'self'", 'data:', 'https://cdn.example.com'],
        connectSrc: ["'self'", 'https://api.example.com'],
      },
    },
  })
);

Spreading the defaults first means object-src 'none', script-src-attr 'none', and all the other hardened defaults stay in place when you only need to add a CDN to img-src.

Steps 6-7: Implement Nonce-Based CSP

A nonce-based CSP is the recommended strict CSP approach according to MDN and the W3C CSP Level 3 specification. The server generates a new cryptographically random value for each HTTP response, embeds it as an attribute on each <script> and <style> tag it wants to allow, and includes nonce-{value} in the script-src directive. The browser runs only those tagged elements. Even if an attacker injects a <script> element, it lacks the correct nonce and the browser silently drops it.

Step 6: Generate a Cryptographically Secure Nonce Per Request

const express = require('express');
const helmet = require('helmet');
const crypto = require('node:crypto');
const { contentSecurityPolicy } = require('helmet');

const app = express();

// Nonce middleware: runs before Helmet, attaches nonce to res.locals
app.use((req, res, next) => {
  res.locals.nonce = crypto.randomBytes(16).toString('base64');
  next();
});

app.use((req, res, next) => {
  helmet({
    contentSecurityPolicy: {
      directives: {
        ...contentSecurityPolicy.getDefaultDirectives(),
        scriptSrc: ["'self'", `'nonce-${res.locals.nonce}'`],
        styleSrc: ["'self'", `'nonce-${res.locals.nonce}'`],
      },
    },
  })(req, res, next);
});

app.get('/', (req, res) => {
  const { nonce } = res.locals;
  res.send(`
    
    
      
        CSP Nonce Demo
        
      
      
        

CSP Nonce Demo

`); }); app.listen(3000, () => console.log('Running on http://localhost:3000'));

A few implementation notes worth paying attention to:

  • crypto.randomBytes(16).toString('base64') produces a 22-character base64 string with 128 bits of entropy. This is well above the minimum entropy recommended by the CSP specification.
  • The nonce must be different for every response. Never cache it or reuse it across requests. If an attacker can predict the nonce, the protection collapses.
  • Because Helmet is called inside a middleware that already has res.locals.nonce, you can pass the nonce value into the directive string. The pattern wraps Helmet in an outer middleware to thread the per-request nonce through.

Step 7: Verify the Nonce in the Browser

Restart the server and reload the page. Open the DevTools Console. You should see:

// Console output:
This script runs because the nonce matches.

// A second entry in the console:
[Blocked] Refused to execute inline script because it violates the following
Content Security Policy directive: "script-src 'self' 'nonce-abc123XYZ...'".

Inspect the Content-Security-Policy response header in the Network tab. You will see something like script-src 'self' 'nonce-3MgFjJtDkA9xz4Q=='. Refresh the page and the nonce value changes on every request. This is exactly the correct behavior.

Steps 8-9: Hash-Based CSP for Static Inline Scripts

Nonces require server-side template rendering on each request. When inline scripts are static and never change, such as a small initialization snippet hard-coded in the HTML, a hash-based allowlist removes the need for per-request nonce generation and works with pre-rendered or cached pages.

Step 8: Generate a Script Hash

The browser calculates a SHA-256 hash of the exact text content inside a <script> tag and compares it to the hashes in script-src. If they match, the script runs. If anything changes, including a single space or newline, the hash no longer matches and the script is blocked. This is both a security feature and a development gotcha.

Generate the hash using node:crypto:

const crypto = require('node:crypto');

const scriptContent = `console.log('app initialized');`;

const hash = crypto
  .createHash('sha256')
  .update(scriptContent, 'utf8')
  .digest('base64');

console.log(`'sha256-${hash}'`);
// Output example: 'sha256-abc123...=='

Step 9: Add the Hash to the CSP Directive

const SCRIPT_HASH = "'sha256-ReplaceWithYourActualHashValue='";

app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        ...contentSecurityPolicy.getDefaultDirectives(),
        scriptSrc: ["'self'", SCRIPT_HASH],
      },
    },
  })
);

app.get('/static-demo', (req, res) => {
  res.send(`
    
    
      
        

Hash CSP Demo

`); });

The browser also reports the expected hash in the console when it blocks an inline script. If you miss or miscalculate a hash, check the DevTools console: it will print exactly the sha256-... string you need to add to the directive. This is one of the few places where browser error messages are genuinely helpful as a configuration tool.

Hash-based allowlisting is also useful for third-party snippets you trust but cannot control, such as analytics initialization code that rarely changes. You take the hash once, add it to the policy, and the script runs without any host-based allowlisting that could be abused if the third-party domain is compromised.

Steps 10-11: Configure CSP Violation Reporting

A CSP without a reporting endpoint is blind. You will have no visibility into blocked requests, misconfigured policies, or active attack attempts. Setting up a reporting endpoint is step 10.

Step 10: Add a Reporting Endpoint

CSP supports two reporting mechanisms. report-uri is the older directive supported by all browsers. report-to is the newer Reporting API-based mechanism that requires a Reporting-Endpoints response header. For maximum coverage, send both during migration:

app.use((req, res, next) => {
  res.locals.nonce = require('node:crypto').randomBytes(16).toString('base64');
  next();
});

app.use((req, res, next) => {
  // Reporting-Endpoints header for the Reporting API (report-to directive)
  res.setHeader(
    'Reporting-Endpoints',
    'csp-violations="https://example.com/csp-reports"'
  );

  helmet({
    contentSecurityPolicy: {
      directives: {
        ...require('helmet').contentSecurityPolicy.getDefaultDirectives(),
        scriptSrc: ["'self'", `'nonce-${res.locals.nonce}'`],
        styleSrc: ["'self'", `'nonce-${res.locals.nonce}'`],
        reportTo: 'csp-violations',                     // CSP Level 3
        reportUri: ['https://example.com/csp-reports'], // legacy fallback
      },
    },
  })(req, res, next);
});

// Endpoint to receive CSP violation reports
app.post('/csp-reports', express.json({ type: 'application/csp-report' }), (req, res) => {
  const report = req.body['csp-report'];
  if (report) {
    console.log('CSP Violation:', {
      blockedUri: report['blocked-uri'],
      violatedDirective: report['violated-directive'],
      documentUri: report['document-uri'],
      timestamp: new Date().toISOString(),
    });
  }
  res.status(204).end();
});

The difference between the two reporting directives matters in 2026. report-uri sends a direct POST request to the specified URL using the deprecated application/csp-report content type. Modern Chromium browsers (version 90+) and Firefox prefer report-to, which uses the Reporting API and batches reports. For a production application, use both: report-uri captures reports from older browsers; report-to is where modern browsers send. Eventually you can drop report-uri when legacy browser support is no longer required.

Step 11: Deploy with Report-Only Mode

Never deploy a new CSP directly to production in enforcement mode on the first push. The correct pattern is to run the policy in report-only mode first: the browser processes the policy, sends reports for any violations, but does not actually block anything. This lets you collect real-world violation data before enforcement breaks legitimate functionality.

app.use((req, res, next) => {
  res.locals.nonce = require('node:crypto').randomBytes(16).toString('base64');
  next();
});

app.use((req, res, next) => {
  helmet({
    contentSecurityPolicy: {
      useDefaults: true,
      directives: {
        scriptSrc: ["'self'", `'nonce-${res.locals.nonce}'`],
        styleSrc: ["'self'", `'nonce-${res.locals.nonce}'`],
        reportUri: ['https://example.com/csp-reports'],
      },
      reportOnly: true, // <-- sets Content-Security-Policy-Report-Only instead
    },
  })(req, res, next);
});

Run report-only mode for at least two weeks in production. Review the reports. Add any missing sources to the policy. Once no new violation types are appearing for 3-5 days straight, remove reportOnly: true and deploy in enforcement mode. If both Content-Security-Policy and Content-Security-Policy-Report-Only headers are present on the same response, the browser enforces the first and only reports on the second. You can use this to gradually migrate one policy to enforcement while testing the next iteration in report-only mode simultaneously.

Step 12: Test and Validate Your CSP Policy

After writing a policy, two tools catch mistakes that are easy to miss during manual review.

Google CSP Evaluator (csp-evaluator.withgoogle.com) is a free online tool from Google's security team. Paste your policy string and it returns a severity-ranked list of issues: unsafe keyword usage, missing directives, host-based allowlists that could be bypassed, and more. Run every policy through this before deploying.

Browser DevTools remain the most direct testing loop. With your server running, open the Network tab, load any page, and click on the main document request. The Response Headers section shows the exact CSP string your server is sending. The Console tab shows any blocked resource attempts with the specific violated directive.

A quick automated test you can add to a CI pipeline checks that the CSP header is present on every response:

// test/csp.test.js (using Node.js built-in test runner)
const assert = require('node:assert');
const { test } = require('node:test');

// Start server on a test port before running
const baseUrl = 'http://localhost:3001';

test('CSP header is present', async () => {
  const response = await fetch(baseUrl + '/');
  const cspHeader = response.headers.get('content-security-policy');
  assert.ok(cspHeader, 'Content-Security-Policy header should be present');
  assert.match(cspHeader, /script-src/, 'CSP should include script-src directive');
  assert.match(cspHeader, /object-src 'none'/, 'CSP should block plugins');
  assert.match(cspHeader, /script-src-attr 'none'/, 'CSP should block inline event handlers');
});

test('CSP nonce changes per request', async () => {
  const [r1, r2] = await Promise.all([
    fetch(baseUrl + '/'),
    fetch(baseUrl + '/'),
  ]);
  const csp1 = r1.headers.get('content-security-policy');
  const csp2 = r2.headers.get('content-security-policy');
  assert.notStrictEqual(csp1, csp2, 'Nonces should differ between requests');
});

Run with node --test test/csp.test.js. This test confirms the header exists and that nonces are rotating. Add it to your CI job so no deploy removes or regresses the policy.

5 Common Pitfalls and How to Fix Them

Most CSP implementation problems fall into predictable patterns. These are the five most common ones in Node.js/Express applications.

Pitfall 1: Using 'unsafe-inline' in script-src. This is the single most damaging mistake you can make. 'unsafe-inline' allows all inline scripts to run, which eliminates the XSS protection CSP provides. Many developers add it to "fix" blocked inline scripts rather than switching to nonces. The Google CSP Evaluator flags this as a critical bypass. The fix is to replace all inline scripts with nonce-based equivalents or external files, never to relax the policy.

Pitfall 2: Forgetting that the nonce must regenerate on every request. A cached page or a static HTML file cannot use nonces correctly because the nonce embedded in the HTML will not match the nonce in the header of the next response. For static sites, use hashes instead of nonces. For server-rendered apps, confirm the nonce generation middleware runs before the template render on every response path, including error handlers and redirects.

Pitfall 3: Omitting object-src 'none'. Even in 2026, browser plugins and <object> elements can execute JavaScript. A policy that sets script-src but leaves object-src unrestricted (or relies on an insufficiently restrictive default-src) still has an attack surface via embedded objects. Always set object-src 'none' explicitly. Helmet 8.x does this by default.

Pitfall 4: Setting base-uri too broadly. The <base> element changes the base URL for all relative links in a document. An attacker who can inject a <base href="https://evil.example/"> into the page head can redirect all subsequent relative resource loads to their domain. Set base-uri 'self' or base-uri 'none' if you never use <base> tags. Helmet includes base-uri 'self' in its defaults.

Pitfall 5: Applying CSP to only some routes. If you call helmet() only on authenticated routes or API routes but not on public pages, those unprotected pages are still vulnerable. CSP must apply to every HTTP response that renders HTML. The correct placement is as the very first middleware in the Express chain, before any routing logic, so it covers every request without exception.

CSP Violations: 8 Troubleshooting Scenarios

When CSP blocks something it should not, the browser console message tells you exactly which directive was violated and which resource was blocked. Here are the 8 most common scenarios in Node.js apps and how to resolve each.

Violation message (truncated)CauseFix
Refused to execute inline script because it violates CSP...Inline <script> tag without a nonce or matching hashAdd the correct nonce attribute to the script tag or switch to an external file
Refused to load the script 'https://cdn.example.com/...'External host not in script-src allowlistAdd the CDN host to scriptSrc or use SRI hashes to whitelist specific files
Refused to apply inline style because it violates CSP (style-src)Inline style="..." attribute or <style> block without nonceAdd nonce to <style> blocks; move inline style attributes to a CSS file
Refused to connect to 'https://api.example.com'...Fetch or XHR target not in connect-srcAdd the API host to connectSrc directive
Refused to load the image '...'Image source not in img-srcAdd the image host or data: if using data URIs
Refused to frame 'https://...'Embed or iframe target blocked by frame-srcAdd the frame host to frameSrc directive
Refused to load font from '...'Web font from CDN not in font-srcAdd font CDN (e.g., Google Fonts) to fontSrc
Refused to execute inline event handler...onclick, onerror, or other inline event attributeMove event logic to an external script file with nonce; never add 'unsafe-hashes'

Two troubleshooting patterns save significant time. First, when violations appear in report-only mode, aggregate them by violated-directive before making policy changes. If you see 200 reports about the same directive from a single third-party script, adding that one source solves all 200. Second, the browser does not report "allowed" resources, only violations. If you cannot tell whether your policy is working, temporarily add a deliberate violation (a script from an unknown host) and confirm it appears in the console. Then remove the deliberate test.

Advanced Tips: strict-dynamic, Trusted Types, and Next.js

The 'strict-dynamic' keyword solves a practical problem with nonce-based CSP: third-party scripts that dynamically create additional <script> elements. Normally, dynamically created scripts inherit no trust from the nonce of the script that created them. With 'strict-dynamic' in script-src, scripts that are trusted (via nonce or hash) can propagate their trust to scripts they create. This is critical for loading analytics, tag managers, or widget loaders that self-install further scripts. Add it to a nonce-based policy like this:

scriptSrc: ["'self'", `'nonce-${res.locals.nonce}'`, "'strict-dynamic'"],

'strict-dynamic' is supported in all modern browsers (Chromium 52+, Firefox 52+, Safari 15.4+). Older browsers that do not understand the keyword fall back to any host-based rules in the directive. For practical purposes in 2026, you can rely on it in production without fallback concerns for modern user agents.

Trusted Types is a complementary browser API that prevents DOM-based XSS by requiring that values assigned to dangerous sinks like innerHTML, document.write, and eval go through a registered "Trusted Types policy." You enable it via CSP using the require-trusted-types-for directive:

directives: {
  ...contentSecurityPolicy.getDefaultDirectives(),
  scriptSrc: ["'self'", `'nonce-${res.locals.nonce}'`],
  requireTrustedTypesFor: ["'script'"],
  trustedTypes: ['default', 'myapp-policy'],
}

Trusted Types and CSP are complementary, not redundant. CSP controls what loads; Trusted Types controls what runs after it loads. For high-security Node.js applications, running both gives defense-in-depth coverage for both the load and execution phases of an XSS attack.

Next.js applications present specific CSP challenges because Next.js injects inline scripts for hydration, module preloading, and the React reconciler during server-side rendering. Next.js 14+ supports a built-in nonce option on its next.config.js that threads the nonce through the framework-generated scripts automatically. Set the nonce in a Next.js middleware file (middleware.ts) using the NextResponse headers API and pass it through the request context. The nonce must match the one in the Content-Security-Policy header set by the same middleware. Do not try to inject it via the Helmet pattern used in Express: Next.js's edge runtime uses a different middleware model.

Single-page applications (SPAs) bundled with Webpack or Vite often inject runtime scripts that break strict CSP. The practical solution is to use 'strict-dynamic' with a nonce on the main bundle entry point, which then propagates trust to all dynamically loaded chunks. Configure your bundler to accept the nonce via a __webpack_nonce__ or Vite-equivalent global variable.

CSP Security Levels Compared

CSP approachXSS protectionBrowser supportComplexityBest for
No CSPNoneN/ANoneNot recommended
Allowlist-based (script-src https://cdn.example.com)Weak (domain hijack bypasses)All browsersLowLegacy apps, quick wins
Nonce-based (strict)StrongChrome 40+, Firefox 31+, Safari 10+MediumServer-rendered apps, Express
Hash-based (strict)StrongChrome 40+, Firefox 31+, Safari 10+MediumStatic sites, cached content
Nonce + strict-dynamicStrongestChrome 52+, Firefox 52+, Safari 15.4+Medium-highSPAs, tag manager apps
Nonce + Trusted TypesDefense in depthChrome 83+, partial FirefoxHighHigh-security applications

FAQ: Content Security Policy in Node.js

Does CSP replace input validation and output encoding?

No. CSP is a defense-in-depth layer, not a replacement for proper input handling. Always validate and sanitize user input on the server side and escape output in templates. CSP acts as a safety net if those primary defenses fail. Running CSP without input validation is like having a good lock on the front door but leaving the windows open.

Can CSP break an existing application?

Yes, and it will if there are inline scripts, inline styles, or resources loaded from unlisted domains. This is why the correct deployment sequence is report-only mode first. Run your application with Content-Security-Policy-Report-Only and collect violations for two weeks before switching to enforced mode. Every violation in report-only mode is something that would break in enforcement mode.

What is the difference between unsafe-inline and unsafe-hashes?

'unsafe-inline' allows all inline scripts and event handlers, providing essentially no XSS protection. 'unsafe-hashes' is more targeted: it allows specific inline event handler values that match a provided hash, letting you keep a specific onclick="return false;" while blocking everything else. Neither should appear in a production CSP that aims for strong security. Move event handling to external script files with nonces instead.

Does CSP slow down the application?

The header itself adds negligible overhead, typically under 1 millisecond per response for a typical CSP string. The nonce generation using crypto.randomBytes(16) is similarly fast: it draws from the OS entropy pool and completes in microseconds. The CSP evaluation runs in the browser, not the server, so server-side performance impact is minimal in practice.

Should I set CSP on API endpoints that return JSON?

Setting CSP on JSON API responses is harmless but not necessary, since the browser only processes CSP on document navigations, not JSON responses. The more important header for API responses is X-Content-Type-Options: nosniff to prevent MIME-type sniffing attacks, which Helmet sets automatically. Focus CSP configuration on routes that return HTML.

How do I allow Google Fonts, Font Awesome, or Bootstrap CDN with a strict CSP?

For Google Fonts, add https://fonts.googleapis.com to style-src and https://fonts.gstatic.com to font-src. For Font Awesome via CDN, add its stylesheet host to style-src and the font file host to font-src. For Bootstrap CSS, add the CDN host to style-src. Avoid adding these domains to script-src unless you are specifically loading JavaScript from those CDNs: wide host permissions in script-src are a common CSP bypass vector because attackers can sometimes find JSONP endpoints on those domains that execute arbitrary JavaScript.

What version of Helmet should I use?

Use Helmet 8.2.0 (current as of June 2026). Helmet 7.x and below did not include script-src-attr 'none' in the default CSP. Helmet 8.x tightened defaults meaningfully. Always keep Helmet updated since each minor release may improve CSP defaults to reflect emerging attack patterns. Check the official Helmet changelog when upgrading.

Is a nonce-based CSP vulnerable if the nonce is guessable?

Yes. The entire security model of nonce-based CSP depends on attackers not being able to predict the nonce value. Using crypto.randomBytes(16) produces 128 bits of cryptographic entropy, which is not guessable. Never generate nonces with Math.random(), timestamps, sequential counters, or user session IDs. Also never cache a nonce across multiple requests or embed it in a cookie: the nonce must be ephemeral to each individual HTTP response.

These articles extend the security concepts covered here into related implementation areas:

Authority references: