ASVS 5.0 requirement V3.4.3 says your HTTP responses must include a Content-Security-Policy header that tells the browser which content it may load and run. At minimum, a global policy must include object-src 'none' and base-uri 'none', and must define either an allowlist or nonces or hashes for scripts. At Level 3, you need a per-response policy that uses nonces or hashes. The base requirement is Level 2.
A policy that only says default-src * or allows 'unsafe-inline' for scripts does not meet the intent.
Why it matters
Output encoding is your first defense against cross-site scripting, and CSP is the backstop for when it fails. If an attacker gets a <script> tag into a page, a well built policy makes the browser refuse to run it because it has no valid nonce and the source is not allowed.
The two mandatory directives close specific holes. object-src 'none' disables legacy plugin content such as embedded objects, which attackers used to run script from otherwise blocked sources. base-uri 'none' stops an injected <base> tag from changing where relative script URLs resolve, which can redirect your own <script src="app.js"> to an attacker's server.
The script rule is the one that does the real work. An allowlist of hostnames is better than nothing but is weak in practice, because many allowed CDNs host scripts an attacker can reuse. Nonces and hashes tie permission to the specific script you wrote.
How to implement it
A strong starting policy, nonce based:
Content-Security-Policy:
default-src 'self';
script-src 'nonce-R4nd0mValue' 'strict-dynamic';
object-src 'none';
base-uri 'none';
frame-ancestors 'self';
report-to csp-endpoint
The nonce must be unpredictable and different on every response. Generate 16 or more random bytes per request.
Express with Helmet:
const crypto = require("crypto");
app.use((req, res, next) => {
res.locals.cspNonce = crypto.randomBytes(16).toString("base64");
next();
});
app.use(helmet.contentSecurityPolicy({
useDefaults: false,
directives: {
defaultSrc: ["'self'"],
scriptSrc: [(req, res) => "'nonce-" + res.locals.cspNonce + "'", "'strict-dynamic'"],
objectSrc: ["'none'"],
baseUri: ["'none'"],
frameAncestors: ["'self'"]
}
}));
Then add the nonce to every legitimate script tag in your templates:
<script nonce="<%= cspNonce %>" src="/static/app.js"></script>
If your pages are fully static and cannot take a per response nonce, use hashes: compute the SHA-256 of each inline script and list it as 'sha256-...'. Static sites can also use an allowlist of their own origin with no inline script at all, which is the simplest meeting of the Level 2 requirement.
Roll out with Content-Security-Policy-Report-Only first, watch the reports for a week, fix inline handlers such as onclick, and then enforce.
Click through it at your own pace, or open it full screen.
How to test it
Check the header on several response types:
curl -sI https://app.example.com/ | grep -i content-security-policy
curl -sI https://app.example.com/login | grep -i content-security-policy
curl -sI https://app.example.com/missing | grep -i content-security-policy
Look for object-src 'none', base-uri 'none', and a script source that is either a hostname list, a nonce or a hash. Flag 'unsafe-inline' and 'unsafe-eval' in script-src, and wildcards.
For a Level 3 check, request the same page twice and confirm the nonce changes:
for i in 1 2; do curl -sI https://app.example.com/ | grep -o "nonce-[A-Za-z0-9+/=]*"; done
Finally, prove the policy blocks something. In a staging copy, add <script>alert(1)</script> to a page template without a nonce and load it in the browser. The console should report a CSP violation and the alert must not fire.
Common mistakes
Policy only on the homepage. Set it globally, in middleware or at the proxy, so every HTML response has one.
Static nonce. A nonce hard coded in config is a value an attacker can read once and reuse.
unsafe-inline to silence errors. It cancels the protection that script-src exists to give.
Allowlisting broad CDNs. Hosts that serve user uploaded or arbitrary packages can supply attacker controlled scripts.
Skipping base-uri. Many policies cover scripts and forget it.
Enforcing without a trial period. Report only mode finds the breakage before users do.
Quick check
Does CSP replace output encoding?
No. Encode output so injection does not happen. CSP limits the damage if you miss a spot.
What does strict-dynamic do?
It lets a script that carries a valid nonce load further scripts without each one being listed, and it makes browsers ignore host allowlists in script-src. That keeps the policy short for modern apps.
Sources
- OWASP ASVS 5.0, requirement V3.4.3
- MDN: Content Security Policy