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.

  • Security
  • $content-security-policy
  • 185 lines
  • 2 files
  • ~1,950 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- content-security-policy

Installs to ~/.agents/skills/content-security-policy/ for every project on this machine.

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.

~/.agents/skills/content-security-policy/SKILL.md

--- name: content-security-policy description: Designs and rolls out a strict Content-Security-Policy using per-response nonces or hashes with 'strict-dynamic', object-src 'none' and base-uri… license: MIT ---

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

SituationPolicy
Server-rendered per request (Rails, Django, Next.js dynamic, Express templates)Nonce-based strict CSP
Static site or SPA shell served from a CDNHash-based strict CSP (hashes of inline scripts), or nonces injected at the edge
Legacy app with many inline handlersReport-only strict CSP now, refactor handlers, then enforce
Need only clickjacking protectionframe-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' disables https:.
  • object-src 'none' and base-uri 'none' close plugin and <base> hijack bypasses.
  • report-to uses the Reporting API; report-uri is 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-uri values like chrome-extension, moz-extension, safari-web-extension and reports whose source-file is an extension.
  • Group by effective-directive + blocked-uri host; fix the top groups.
  • inline violations 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

DirectiveWhy
frame-ancestorsClickjacking; replaces X-Frame-Options. Ignored in <meta> CSP
form-actionStops injected forms posting credentials elsewhere
base-uriStops <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.yamlgenerated from this page

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.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem