VNX-NODE-028 – Missing Content-Security-Policy header
Overview
This rule detects calls to res.writeHead(), res.setHeader(), and res.header() (the Express alias) in JavaScript files where no Content-Security-Policy, X-Content-Security-Policy, or X-WebKit-CSP header is being set. Content Security Policy is a browser-enforced allowlist that restricts the origins from which scripts, styles, images, and other resources may be loaded. Without a CSP header, the browser applies no origin restrictions on resource loading, providing no secondary defence if XSS is achieved.
CSP does not prevent XSS from occurring — it is a defense-in-depth control that limits the impact of XSS that does occur. A strict CSP that prohibits inline scripts and restricts script sources to trusted domains prevents the most common post-XSS actions (data exfiltration, credential harvesting) by blocking the injected script from making outbound requests or loading attacker-controlled resources. This vulnerability is classified as CWE-693 (Protection Mechanism Failure).
Severity: Medium | CWE: CWE-693 – Protection Mechanism Failure | OWASP: A05:2021 – Security Misconfiguration | CAPEC: CAPEC-63 – Cross-Site Scripting (XSS)
Why This Matters
Modern browser security models rely on security headers as a key layer of defense. The absence of a CSP means that if any XSS vulnerability exists elsewhere in the application (or in a third-party script), the attacker’s injected code operates with unrestricted capabilities: it can load arbitrary external scripts, exfiltrate data to any domain, inject UI elements, or redirect the page. CSP reports (via report-uri or report-to) also provide runtime visibility into XSS attempts, which is lost without the header.
Regulators and security auditors (PCI-DSS, SOC 2 assessors) increasingly flag missing security headers as findings. Bug bounty programs typically accept missing CSP on sensitive pages as a valid low-to-medium severity report. Implementing CSP is a high-leverage control that pays dividends across all XSS vectors simultaneously.
What Gets Flagged
// FLAGGED: writeHead with no CSP header
res.writeHead(200, {
'Content-Type': 'text/html',
'X-Frame-Options': 'DENY',
});
// FLAGGED: setHeader call loop with no CSP set
res.setHeader('Content-Type', 'text/html');
res.setHeader('Cache-Control', 'no-store');
// FLAGGED: Express res.header without CSP
app.get('/', (req, res) => {
res.header('X-Powered-By', 'MyApp');
res.send(html);
});
Remediation
- Use the helmet middleware for Express, which sets a conservative CSP and other security headers by default with a single
app.use(helmet())call. - If helmet is not suitable, set a CSP manually via
res.setHeader('Content-Security-Policy', '...')with a policy appropriate to your application’s resource requirements. - Start with a report-only policy (
Content-Security-Policy-Report-Only) to identify violations before enforcing, then tighten to enforcement mode. - Avoid
unsafe-inlineandunsafe-evalin script-src; instead use nonces or hashes for inline scripts.
// SAFE: helmet applies a strong CSP and other security headers
const helmet = require('helmet');
const express = require('express');
const app = express();
app.use(helmet());
// SAFE: manual CSP header for custom policies
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy',
[
"default-src 'self'",
"script-src 'self' 'nonce-{RANDOM}'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: https:",
"object-src 'none'",
"base-uri 'self'",
"form-action 'self'",
].join('; ')
);
next();
});