express-csp-header and helmet-csp are both middleware solutions for setting Content Security Policy (CSP) headers in Node.js Express applications, but they serve different architectural needs. express-csp-header is a lightweight, standalone package focused exclusively on generating CSP header strings with support for nonces and strict-dynamic patterns. It provides fine-grained control over policy construction without imposing other security headers. helmet-csp was historically the CSP module within the broader helmet security suite, designed to apply a set of secure defaults alongside other HTTP security headers. However, helmet-csp as a standalone package is deprecated; CSP functionality has been fully integrated into the main helmet package since version 4. Developers today should choose between using the dedicated express-csp-header for custom CSP logic or the unified helmet package (which includes CSP) for comprehensive security hardening.
When securing an Express application, setting a Content Security Policy (CSP) header is one of the most effective ways to prevent cross-site scripting (XSS) and data injection attacks. Two common names appear in this space: express-csp-header and helmet-csp. However, the landscape has shifted significantly in recent years. Let's cut through the confusion and look at how these tools actually work, their current status, and which one fits your architecture.
Before diving into code, there is a vital fact every architect must know: helmet-csp as a standalone package is deprecated.
Prior to version 4, the helmet library was split into many small sub-packages like helmet-csp, helmet-xss-filter, and helmet-frameguard. This allowed developers to pick and choose specific headers. However, starting with helmet v4 (released in 2020), all these sub-modules were merged back into the main helmet package.
The standalone helmet-csp package is no longer maintained. Installing it today will trigger deprecation warnings, and it will not receive security updates or feature improvements. If your project currently uses npm install helmet-csp, you should migrate to the main helmet package immediately.
# ❌ Deprecated - Do not use in new projects
npm install helmet-csp
# ✅ Current Standard - Includes CSP and other headers
npm install helmet
Therefore, the real comparison for modern development is between express-csp-header (a dedicated, single-purpose tool) and helmet (the comprehensive security suite that now contains CSP logic).
The core difference lies in philosophy. express-csp-header does one thing and does it deeply: it builds CSP strings. helmet takes a holistic approach, setting CSP along with 10+ other critical security headers like Strict-Transport-Security, X-Content-Type-Options, and Referrer-Policy in a single call.
With express-csp-header, you manually construct the policy object. This gives you explicit visibility into every directive. It is particularly powerful when you need to generate nonces (random tokens) for inline scripts on every request, a common requirement in server-side rendered (SSR) applications.
import express from 'express';
import { CSP } from 'express-csp-header';
const app = express();
app.use((req, res, next) => {
// Generate a unique nonce for this request
const nonce = Buffer.from(Math.random().toString(36)).toString('base64');
// Attach nonce to locals for use in templates
res.locals.nonce = nonce;
// Apply CSP middleware with custom policy
CSP.middleware({
directives: {
'default-src': [CSP.SELF],
'script-src': [CSP.SELF, (req, res) => `'nonce-${res.locals.nonce}'`],
'style-src': [CSP.SELF, 'unsafe-inline'],
'img-src': [CSP.SELF, 'data:', 'https://cdn.example.com']
}
})(req, res, next);
});
Notice how you define the policy explicitly. There are no hidden defaults. If you don't specify a directive, it isn't set (unless the browser applies its own fallback). This is great for fine-tuning but requires you to know exactly what you need.
helmet simplifies the process by applying a set of secure defaults immediately. You can still customize the CSP, but the starting point is much more restrictive and secure out of the box.
import express from 'express';
import helmet from 'helmet';
const app = express();
// Enables all standard security headers including CSP
app.use(helmet());
// OR: Customize only the CSP part while keeping other defaults
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // Less secure, but sometimes needed
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https://cdn.example.com"]
}
}
})
);
In this example, helmet() automatically sets headers like X-DNS-Prefetch-Control, X-Frame-Options, and Strict-Transport-Security without extra code. If you used express-csp-header, you would need separate packages or manual code to set those.
Dynamic values like nonces are essential for allowing specific inline scripts while blocking others. Both libraries support this, but the syntax differs slightly.
express-csp-header passes the req and res objects directly to the directive value function, making it very straightforward to access response locals.
// express-csp-header approach
const directives = {
'script-src': [
CSP.SELF,
(req, res) => `'nonce-${res.locals.nonce}'`
]
};
helmet uses a similar function-based approach within its configuration object. The function receives the request and response, allowing you to inject dynamic values seamlessly.
// helmet approach
app.use(
helmet({
contentSecurityPolicy: {
directives: {
scriptSrc: [
"'self'",
(req, res) => `'nonce-${res.locals.nonce}'`
]
}
}
})
);
Both achieve the same result. The choice here is less about capability and more about whether you want the CSP logic isolated (express-csp-header) or bundled with other protections (helmet).
Select express-csp-header if:
helmet yet.helmet is undesirable (though the size difference is usually negligible).Select helmet if:
| Feature | express-csp-header | helmet (includes CSP) |
|---|---|---|
| Primary Focus | CSP only | Full suite of HTTP security headers |
| Maintenance Status | ✅ Active | ✅ Active (helmet-csp sub-package is ❌ Deprecated) |
| Default Directives | None (Opt-in) | Secure defaults provided |
| Nonce Support | ✅ Yes (via functions) | ✅ Yes (via functions) |
| Other Headers | ❌ No (Need separate libs) | ✅ Yes (HSTS, X-Frame, etc.) |
| Configuration Style | Explicit policy object | Options object with defaults |
| Best For | Custom, granular control | Rapid, comprehensive hardening |
For the vast majority of professional Express applications, helmet is the correct choice.
Security is rarely just about CSP. An application with a perfect CSP but missing X-Content-Type-Options or Strict-Transport-Security is still vulnerable to other classes of attacks. helmet ensures these protections are applied consistently. The deprecation of the standalone helmet-csp package signals a clear direction from the maintainers: security headers work best when managed as a unified system.
Use express-csp-header only if you have a specific architectural requirement that prevents you from using helmet's defaults or if you are maintaining a legacy system where introducing helmet would cause breaking changes with existing header logic.
If you are currently using helmet-csp, migration is simple:
npm uninstall helmet-cspnpm install helmetimport csp from 'helmet-csp' to import helmet from 'helmet'helmet() call).// Old way (Deprecated)
import csp from 'helmet-csp';
app.use(csp({ directives: { ... } }));
// New way (Recommended)
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: { ... }
}
}));
By consolidating your security headers under helmet, you simplify your dependency tree and align with current best practices in the Node.js ecosystem.
Do not choose helmet-csp as a standalone package for new projects because it is officially deprecated. The CSP functionality previously found in helmet-csp has been merged into the main helmet package. If you want the convenience of automatically setting multiple security headers (including CSP) with secure defaults, use helmet directly. This approach reduces dependency count and ensures all security headers work together cohesively. Only consider the old helmet-csp if you are maintaining a legacy codebase that has not yet migrated to helmet v4 or higher.
Choose express-csp-header if you need precise control over your Content Security Policy without the overhead of other security headers. It is ideal for applications that already handle security headers manually or use other specialized libraries for HSTS, X-Frame-Options, etc. This package shines when you need to generate nonces dynamically for inline scripts or implement strict-dynamic policies in complex server-side rendering setups. It is also the right choice if you want to avoid the opinionated defaults that come with larger security suites.
The Content-Security-Policy header mitigates a large number of attacks, such as cross-site scripting. See MDN's introductory article on Content Security Policy.
This header is powerful but likely requires some configuration for your specific app.
To configure this header, pass an object with a nested directives object. Each key is a directive name in camel case (such as defaultSrc) or kebab case (such as default-src). Each value is an array (or other iterable) of strings or functions for that directive. If a function appears in the array, it will be called with the request and response objects.
const contentSecurityPolicy = require("helmet-csp");
// Sets all of the defaults, but overrides `script-src`
// and disables the default `style-src`.
app.use(
contentSecurityPolicy({
directives: {
"script-src": ["'self'", "example.com"],
"style-src": null,
},
}),
);
// Sets the `script-src` directive to
// "'self' 'nonce-e33cc...'"
// (or similar)
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(32).toString("hex");
next();
});
app.use(
contentSecurityPolicy({
directives: {
scriptSrc: ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`],
},
}),
);
These directives are merged into a default policy, which you can disable by setting useDefaults to false.
// Sets "Content-Security-Policy: default-src 'self';
// script-src 'self' example.com;object-src 'none';
// upgrade-insecure-requests"
app.use(
contentSecurityPolicy({
useDefaults: false,
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "example.com"],
objectSrc: ["'none'"],
upgradeInsecureRequests: [],
},
}),
);
You can get the default directives object with contentSecurityPolicy.getDefaultDirectives(). Here is the default 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
The default-src directive can be explicitly disabled by setting its value to contentSecurityPolicy.dangerouslyDisableDefaultSrc, but this is not recommended.
You can set the Content-Security-Policy-Report-Only instead:
// Sets the Content-Security-Policy-Report-Only header
app.use(
contentSecurityPolicy({
directives: {/* ... */},
reportOnly: true,
}),
);
upgrade-insecure-requests, a directive that causes browsers to upgrade HTTP to HTTPS, is set by default. You may wish to avoid this in development, as you may not be developing with HTTPS. Notably, Safari will upgrade http://localhost to https://localhost, which can cause problems. To work around this, you may wish to disable the upgrade-insecure-requests directive in development. For example:
const isDevelopment = app.get("env") === "development";
app.use(
contentSecurityPolicy({
directives: {
// Disable upgrade-insecure-requests in development.
"upgrade-insecure-requests": isDevelopment ? null : [],
},
}),
);
This module performs very little validation on your CSP. You should rely on CSP checkers like CSP Evaluator instead.