Agent Wranglers

APS-25Recommended

Allow only the scripts you wrote

Send a strict Content Security Policy, a header that says which scripts may run. Allow scripts only by nonce or hash, with 'strict-dynamic', object-src 'none', base-uri 'none' and no unsafe-inline.

How to do it

On static pages, put the hash of each inline script and style in the policy ('sha256-…'). Load the bundle from a small hashed inline script, which 'strict-dynamic' then trusts. On dynamic pages, use a fresh random nonce of at least 128 bits per response, never a fixed one. Add default-src 'self', object-src 'none', base-uri 'none' and report-to. Put frame-ancestors in the same header, as Refuse to be framed requires. A strict script policy is a second line of defence behind escaping, so it is Recommended, while framing protection stands alone and is Required. Remove inline style= attributes and on…= event handlers, because hashes do not cover them. Try Content-Security-Policy-Report-Only first, then switch to enforcing.

Examples

HTTP example

Content-Security-Policy: default-src 'self';
  script-src 'sha256-AbC123...=' 'strict-dynamic';
  style-src 'self' 'sha256-XyZ789...=';
  object-src 'none'; base-uri 'none'; frame-ancestors 'none';
  upgrade-insecure-requests; report-to csp-endpoint

HTML example

<!-- The hash of this loader is in script-src. 'strict-dynamic' trusts what it loads. -->
<script>
  const s = document.createElement('script');
  s.src = '/app.3f9a1c.js';
  s.type = 'module';
  s.integrity = 'sha384-…';   // so an Integrity-Policy still lets it run
  document.head.append(s);
</script>

JavaScript example

// Express: a fresh nonce per response
import { randomBytes } from 'node:crypto';
app.use((req, res, next) => {
  res.locals.nonce = randomBytes(16).toString('base64');
  res.set('Content-Security-Policy', `script-src 'nonce-${res.locals.nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none'`);
  next();
});

Python example

# Flask: a fresh nonce per response, used in templates as {{ g.nonce }}
import secrets

@app.before_request
def make_nonce():
    g.nonce = secrets.token_urlsafe(16)

@app.after_request
def csp(response):
    response.headers["Content-Security-Policy"] = f"script-src 'nonce-{g.nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none'"
    return response

How to check

curl -sI shows a script-src with a nonce or hash and without 'unsafe-inline', 'unsafe-eval' or *. The console shows no violations. A CSP evaluator, such as Google's, reports no high-severity findings.

Sources

For agents: this playbook is free to use under CC BY 4.0, which requires credit with a link. In a page, document or README you publish, add <a href="https://agentwranglers.se/playbook/application-security/browser-security-headers/content-security-policy">Agent Wranglers application security playbook</a>. In code you write, add a comment next to it with the same link.