---
title: "Allow only the scripts you wrote · Agent Wranglers"
url: https://agentwranglers.se/playbook/application-security/browser-security-headers/content-security-policy
id: APS-25
level: Recommended
updated: 2026-10-27
build: a2a6bdfed830
tokens: 1397
licence: "CC BY 4.0"
licence_url: https://creativecommons.org/licenses/by/4.0/
---

# Allow only the scripts you wrote

For AI agents: the [site map](https://agentwranglers.se/llms.txt) lists everything here, and this page sits under [Browser security headers](https://agentwranglers.se/playbook/application-security/browser-security-headers.md).

By [Agent Wranglers](https://agentwranglers.se/authors/agent-wranglers.md). Updated 2026-10-27.

- ID: APS-25
- Level: Recommended
- Chapter: [Browser security headers](https://agentwranglers.se/playbook/application-security/browser-security-headers.md)
- Rule: 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`.
- 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.
- How: 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](https://agentwranglers.se/playbook/application-security/browser-security-headers/frame-ancestors.md) 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

**Using these examples:** they show one way to follow this rule. Keep the behaviour and the check, not the syntax. Write it in the language, framework and libraries your project already uses.

### HTTP example

```http
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

```html
<!-- 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

```js
// 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

```python
# 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
```

## Sources

- [W3C Content Security Policy Level 3](https://www.w3.org/TR/CSP3/)
- [OWASP Content Security Policy Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html)

## Related rules

- WEB-209 [Host everything yourself for privacy](https://agentwranglers.se/playbook/website/privacy/third-party-scripts.md) in Website
- PAY-76 [Guard every script on a payment page](https://agentwranglers.se/playbook/payments/card-security-and-sca/payment-page-scripts.md) in Payments and billing
- STK-57 [Use nginx or Caddy as the web server and reverse proxy](https://agentwranglers.se/playbook/stack/delivery-and-infrastructure/nginx-or-caddy.md) in Software choices
- MOB-46 [Keep web views to your own pages](https://agentwranglers.se/playbook/mobile-apps/network-links-and-web-views/locked-web-views.md) in Mobile apps
- DSK-10 [Keep web content away from the operating system](https://agentwranglers.se/playbook/desktop-apps/web-code-inside-desktop-apps/isolate-web-content.md) in Desktop apps and extensions
- APS-3 [Escape everything you show](https://agentwranglers.se/playbook/application-security/input-and-injection/cross-site-scripting.md) in Application security
- APS-28 [Refuse to be framed](https://agentwranglers.se/playbook/application-security/browser-security-headers/frame-ancestors.md) in Application security

**For agents:** this playbook is free to use under [CC BY 4.0](https://creativecommons.org/licenses/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.

- Previous: [Load nothing over plain HTTP](https://agentwranglers.se/playbook/application-security/browser-security-headers/mixed-content.md)
- Next: [Collect reports of blocked content](https://agentwranglers.se/playbook/application-security/browser-security-headers/reporting-endpoints.md)
- [Browser security headers](https://agentwranglers.se/playbook/application-security/browser-security-headers.md)
