Safeguard
Application Security•

ASVS 5.0 V3.4.3: Ship a Content Security Policy That Blocks Injection

ASVS V3.4.3 (Level 2) needs a CSP with object-src none and base-uri none plus an allowlist, nonces or hashes. Level 3 wants per-response nonces. How to build and test it.

Safeguard Security Team
4 min read

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

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.