Content Security Policy skill for Codex
Strict CSP without breaking checkout. Codex replaces host-allowlist CSPs, which attackers routinely bypass through JSONP and public CDNs, with a strict policy built on per-response nonces or build-time hashes and 'strict-dynamic'. It ships the policy in Report-Only first, filters extension noise out of violation reports, refactors inline handlers, and asks before switching production to enforcement.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- content-security-policy Installs to ~/.agents/skills/content-security-policy/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install content-security-policy The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project content-security-policy Installs to .agents/skills/content-security-policy/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/content-security-policy.zipunzip -o content-security-policy.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) content-security-policy PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $content-security-policy, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Designs and rolls out a strict Content-Security-Policy using per-response nonces or hashes with 'strict-dynamic', object-src 'none' and base-uri 'none', starting in Report-Only with a reporting endpoint and moving to enforcement once violations are triaged. Covers Next.js middleware nonces, Express helmet, nginx, static-site hashes, frame-ancestors and report noise. Use when adding or tightening CSP, removing 'unsafe-inline', fixing CSP violations that break pages, or reviewing a CSP header for bypasses.
Use it when
- Adding a first CSP or replacing an allowlist-based one
- Removing 'unsafe-inline' or 'unsafe-eval'
- Pages break after a CSP change and you need to triage violations
- Reviewing a CSP header for bypasses
Use something else when
- You need the full set of HTTP security headers audited: use security-headers
- The underlying XSS bug still needs fixing in templates: use xss-prevention
How to check the result
The header carries a nonce that changes per request and appears on every script tag, an injected inline script is blocked in the browser console, and CSP Evaluator reports no high-severity issues.
The mistake it prevents
Caching nonce-bearing HTML at a CDN, which serves the same nonce to every visitor and makes it reusable by an attacker.
Done well means: the enforced policy blocks injected inline and third-party script even if an XSS bug exists, it does not rely on host allowlists that attackers can abuse, and the rollout broke nothing because every violation was seen in report-only first.
Inspect first
rg -n -i "content-security-policy|helmet|csp|nonce" -g '!node_modules' -g '!*.lock'
curl -sI https://localhost:3000/ | grep -i -E "content-security-policy|report"
rg -n "<script(?![^>]*\bsrc=)" -P -g '*.{html,erb,jinja,j2,ejs,hbs,tsx,jsx,vue,svelte,php}' | head
rg -n " on[a-z]+=\"" -g '*.{html,erb,jinja,ejs,hbs,php}' | head # inline handlers
rg -n "javascript:" -g '!node_modules' | head
Collect: is HTML rendered per request (nonces possible) or fully static (hashes)? Which
third-party scripts load (analytics, tag managers, chat, payments)? Are there inline
event handlers or javascript: URLs (incompatible with a strict policy)?
Choose the policy shape
| Situation | Policy |
|---|---|
| Server-rendered per request (Rails, Django, Next.js dynamic, Express templates) | Nonce-based strict CSP |
| Static site or SPA shell served from a CDN | Hash-based strict CSP (hashes of inline scripts), or nonces injected at the edge |
| Legacy app with many inline handlers | Report-only strict CSP now, refactor handlers, then enforce |
| Need only clickjacking protection | frame-ancestors alone is fine as a first step |
Strict policy template
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic' https: 'unsafe-inline';
object-src 'none';
base-uri 'none';
frame-ancestors 'self';
form-action 'self';
upgrade-insecure-requests;
report-to csp;
report-uri /csp-reports
Reporting-Endpoints: csp="/csp-reports"
(Send it as one header line; it is wrapped here for reading.)
How the parts interact:
'nonce-...'allows only script tags carrying that nonce.'strict-dynamic'lets those trusted scripts load further scripts (tag managers, bundler chunks) and makes modern browsers ignore host lists and'unsafe-inline'.https:and'unsafe-inline'are fallbacks for very old browsers only; browsers that understand nonces ignore'unsafe-inline', and'strict-dynamic'disableshttps:.object-src 'none'andbase-uri 'none'close plugin and<base>hijack bypasses.report-touses the Reporting API;report-uriis deprecated but still the only one some browsers honour. Send both.
Avoid allowlist-style policies (script-src 'self' cdn.example.com *.googleapis.com).
Large CDNs and Google domains host JSONP endpoints and old Angular builds that turn an
allowlist into a bypass.
Nonce implementation
Generate 128 bits from a CSPRNG per response, put it in the header and on every
<script> tag, and never cache the HTML with a nonce in it.
Express with helmet:
import crypto from "node:crypto";
import helmet from "helmet";
app.use((req, res, next) => { res.locals.cspNonce = crypto.randomBytes(16).toString("base64"); next(); });
app.use(helmet({
contentSecurityPolicy: {
useDefaults: false,
directives: {
scriptSrc: [(req, res) => `'nonce-${res.locals.cspNonce}'`, "'strict-dynamic'", "https:", "'unsafe-inline'"],
objectSrc: ["'none'"],
baseUri: ["'none'"],
frameAncestors: ["'self'"],
reportUri: ["/csp-reports"],
},
reportOnly: true, // flip to false after triage
},
}));
// template: <script nonce="<%= cspNonce %>" src="/app.js"></script>
Next.js App Router: generate the nonce in middleware.ts, set it on the request headers
(x-nonce) and the response CSP header; Next.js applies it to its own scripts when the
CSP header is present on the request, and you read it with headers() for your own
<Script nonce>. Using nonces forces dynamic rendering for those pages (no static
optimisation, no ISR caching of HTML); weigh that before choosing nonces over hashes.
Django: django-csp provides request.csp_nonce. Rails: content_security_policy
initializer with content_security_policy_nonce_generator and javascript_tag nonce: true.
Hash implementation (static HTML)
# sha256 of the exact text between <script> and </script>, byte for byte (incl. newlines)
openssl dgst -sha256 -binary inline-snippet.txt | base64
Policy: script-src 'sha256-<b64>' 'strict-dynamic'; object-src 'none'; base-uri 'none'.
Any whitespace change in the inline script changes the hash; generate hashes in the build,
not by hand.
Rollout
- [ ] 1. Ship the strict policy as Content-Security-Policy-Report-Only
- [ ] 2. Collect reports for at least one full traffic cycle (a week, plus month-end jobs)
- [ ] 3. Triage: own code -> fix; needed third party -> nonce/strict-dynamic handles it; extensions -> ignore
- [ ] 4. Refactor inline handlers (onclick=) and javascript: URLs to addEventListener
- [ ] 5. Enforce, keeping report-only for the next tightening step (e.g. Trusted Types)
- [ ] 6. Alert on a sudden rise in violations after deploys
Ask the user before switching a production policy from report-only to enforce; a wrong policy can blank checkout or login pages.
Report triage rules:
- Ignore
blocked-urivalues likechrome-extension,moz-extension,safari-web-extensionand reports whosesource-fileis an extension. - Group by
effective-directive+blocked-urihost; fix the top groups. inlineviolations from your own pages mean a missing nonce or an inline handler.- The report endpoint accepts unauthenticated POSTs: rate-limit it, cap body size, never render report contents unescaped in an admin UI.
Directives people forget
| Directive | Why |
|---|---|
frame-ancestors | Clickjacking; replaces X-Frame-Options. Ignored in <meta> CSP |
form-action | Stops injected forms posting credentials elsewhere |
base-uri | Stops <base href> hijacking relative script URLs |
object-src 'none' | Plugins; still required in strict policies |
require-trusted-types-for 'script' | DOM XSS enforcement; see $xss-prevention |
<meta http-equiv="Content-Security-Policy"> cannot carry frame-ancestors,
report-uri or sandbox, and only applies after it is parsed. Prefer the header.
Verification
curl -sI https://localhost:3000/ | grep -i content-security-policy
curl -s https://localhost:3000/ | grep -o 'nonce="[^"]*"' | sort | uniq -c # every script has the nonce
for i in 1 2; do curl -sI https://localhost:3000/ | grep -io "nonce-[A-Za-z0-9+/=]*"; done # differs per request
Then in a local browser: inject <script>alert(1)</script> through a known reflection
point (or the DevTools console with document.body.insertAdjacentHTML) and confirm the
console shows a CSP block. Run Google’s CSP Evaluator against the final header and resolve
every high-severity note.
Output template
## CSP: <app>
Mode: report-only | enforced Type: nonce | hash
Policy: <header value>
Violations triaged: <n groups>, fixed <n>, ignored (extensions) <n>
Inline handlers refactored: <files>
Next step: <Trusted Types, tighten connect-src, ...>
Gotchas
- CDN or reverse-proxy caching of HTML freezes one nonce for everyone: an attacker reads it and reuses it. Disable HTML caching or move nonce injection to the edge.
- Two CSP headers are both enforced (intersection); a framework default plus your own header often blocks everything.
'unsafe-eval'is needed by some template compilers and older libraries; scope it to the pages that need it rather than site-wide.- Nonces in
<script nonce>are hidden from DOM reads by browsers, but server-side templating that echoes the nonce into other attributes leaks it.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Content Security Policy"
short_description: "Strict CSP without breaking checkout"
default_prompt: "$content-security-policy Add a strict Content-Security-Policy to our Next.js app without breaking analytics" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.