CSP strict-dynamic explained

This guide is part of the Content-Security-Policy (CSP) reference and covers the 'strict-dynamic' source expression — the keyword that turns a nonce or hash allowlist into a self-propagating trust chain. With 'strict-dynamic' in script-src, a script you explicitly trust (via its nonce or hash) is allowed to load further scripts, and those inherit the trust without needing to be on any host allowlist. This is the foundation of a CSP3 “strict” policy: instead of maintaining a brittle list of CDN hostnames that an attacker can often abuse for bypass, you trust one root script and let it vouch for what it pulls in.

Configuration Syntax & Exact Values

The canonical CSP3 strict policy combines 'strict-dynamic', a nonce (or hash), and two backwards-compatibility fallbacks:

Content-Security-Policy: script-src 'strict-dynamic' 'nonce-r4nd0mBase64Value' https: 'unsafe-inline'; object-src 'none'; base-uri 'self'

Annotated breakdown — note that the tokens deliberately mean different things to different browser generations:

The result is one policy that is strict on modern browsers and gracefully degrades on old ones — modern engines enforce nonce + propagation, older engines enforce https: + 'unsafe-inline'.

strict-dynamic trust propagation A nonced root script is trusted by the policy; under strict-dynamic the scripts it loads inherit that trust, while a script that the page itself injects without the nonce is blocked. Root script nonce-X trusted Loaded child A inherits trust Loaded child B inherits trust Injected, no nonce blocked
Trust flows from the nonced root script to the scripts it loads; markup or attacker-injected scripts without that lineage are still blocked.

Server-Side Configuration

The header is static apart from the nonce, which must be generated per request (see generating CSP nonces per request). The placeholder 'nonce-…' below stands for that per-response value.

Nginx

add_header Content-Security-Policy "script-src 'strict-dynamic' 'nonce-$request_id' https: 'unsafe-inline'; object-src 'none'; base-uri 'self'" always;

always emits the header on error responses too. $request_id is a stopgap; prefer an application-generated cryptographic nonce as described in the noncing guide, and mark these responses uncacheable.

Apache

Header always set Content-Security-Policy "script-src 'strict-dynamic' 'nonce-%{REQUEST_NONCE}e' https: 'unsafe-inline'; object-src 'none'; base-uri 'self'"

Header always set attaches the header across all status codes; requires mod_headers. %{REQUEST_NONCE}e reads an environment variable your app sets per request — the nonce must come from the application layer, not a static config value.

Node/Express (Helmet)

const crypto = require('crypto');
const helmet = require('helmet');

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

app.use(helmet.contentSecurityPolicy({
  useDefaults: false,
  directives: {
    scriptSrc: [
      "'strict-dynamic'",
      (req, res) => `'nonce-${res.locals.nonce}'`,
      "https:",
      "'unsafe-inline'",
    ],
    objectSrc: ["'none'"],
    baseUri: ["'self'"],
  },
}));

The scriptSrc function is evaluated per response so the nonce matches the value rendered into the root <script nonce="…">. Order does not affect enforcement, but keep 'strict-dynamic' and the nonce together for readability.

Diagnostic & Verification Steps

Confirm the policy ships, then check that propagation works and that the fallbacks are present.

curl -sI https://example.com/ | grep -i content-security-policy

Expected output:

content-security-policy: script-src 'strict-dynamic' 'nonce-Yz2bQk9...' https: 'unsafe-inline'; object-src 'none'; base-uri 'self'

Paste the policy into Google’s CSP Evaluator (csp-evaluator.withgoogle.com). A correct strict policy scores well and flags https:/'unsafe-inline' as ignored on modern browsers (the intended fallback), rather than as live weaknesses. The Evaluator warns loudly if the nonce or hash is missing — that is the failure to fix, because 'strict-dynamic' without an entry point trusts nothing.

In the browser DevTools → Console, expected behavior:

Edge Cases, Security Implications & Safe Rollback

Rollback (reversible): the change is header-only and non-destructive. To revert, drop 'strict-dynamic' and return to your prior explicit script-src host allowlist (plus the nonce/hash), then reload the service. Browsers immediately resume enforcing the host list.

# Revert: remove 'strict-dynamic', restore the explicit host allowlist, then:
# nginx -t && systemctl reload nginx

Frequently Asked Questions

Why are https: and 'unsafe-inline' in a “strict” policy? They are backwards-compatibility fallbacks, not active permissions on modern browsers. A CSP3 browser sees 'strict-dynamic' and a nonce and ignores both https: and 'unsafe-inline'. Older browsers that do not understand 'strict-dynamic' (or nonces) fall back to honoring them, so scripts still load there. The policy is strict where it can be and degrades gracefully where it cannot.

Does 'strict-dynamic' mean I no longer need a nonce? No — the opposite. 'strict-dynamic' is inert without a nonce or hash to seed trust. You still generate a per-request nonce (or use a build-time hash) for the root script; 'strict-dynamic' only governs how that trust spreads to the scripts the root loads.

Why is my dynamically loaded script still blocked? Two common causes: the loader uses document.write, which 'strict-dynamic' does not trust — switch to document.createElement('script') plus appendChild; or the chain breaks because an intermediate script was injected without the nonce and could not become trusted in the first place. Trust only propagates from an already-trusted script.

Can I use 'strict-dynamic' with hashes instead of nonces? Yes. A 'sha256-…' hash of the inline root script works as the entry point exactly like a nonce, which is useful for static, cacheable pages with no per-request render step. Everything the hashed root then loads inherits trust the same way.

Conclusion

Roll 'strict-dynamic' out incrementally. Deploy the strict policy in Content-Security-Policy-Report-Only on staging first so any blocked loader or document.write surfaces as a report instead of breakage, validate it in CSP Evaluator and confirm dynamically loaded scripts run, then promote to enforcing mode on production while keeping the https:/'unsafe-inline' fallbacks for older clients until your traffic no longer needs them.