Content Security Policy tells a supporting browser which actions a document permits. Fetch directives govern categories such as scripts, styles, and images; other directives cover different boundaries. The policy is supplied with the document, commonly through a response header. It adds constraints to browser behavior, but does not repair an injection bug, authenticate application users, or promise that every possible form of script execution has been eliminated.

An omitted directive is not universally a denial. Some fetch directives fall back to default-src, while directives such as frame-ancestors, base-uri, and form-action need their own consideration. A policy containing only a script restriction is not automatically a complete restriction on images, framing, and form submission. Read each directive’s scope instead of treating the presence of a CSP header as one indivisible security guarantee.

Authorize the intended script

A nonce policy gives specifically selected script elements an unpredictable value also named in the policy. Generate a fresh value for each response and place it only on the scripts the server intends to authorize. Automatically adding a nonce to every discovered script element would also bless injected elements. A reused build-time nonce loses the per-response unpredictability that this design depends on.

Source expressions in one directive generally express alternatives. Combining an allowed host with a nonce does not mean a script must satisfy both conditions. Fixed static pages can instead use hashes for approved script contents, with changes to the exact content requiring corresponding policy updates. Choose a policy suited to how the page is produced; weakening it with broad inline execution permission can remove the protection the nonce was supposed to provide.

When it breaks

Legitimate inline scripts, event handlers, generated content, or previously unnoticed dependencies can stop working when enforcement begins. Content-Security-Policy-Report-Only helps observe violations while permitting the actions, so successful execution under report-only is not proof that an enforcing policy will permit the page. Reports also require interpretation: an image violation is not evidence that the tested inline script was blocked.

Multiple enforcing policies apply together. Adding a more permissive header at a proxy does not relax an existing restrictive policy; an action must satisfy every applicable enforcing policy. This can explain a valid-looking nonce that still fails. Inspect all delivered policy headers and the browser’s effective directive. Fix the policy’s intended boundary or the legitimate resource usage rather than assuming the last header replaces every earlier restriction.

Check it quickly

Save the script as csp-nonce.py, run python3 csp-nonce.py --serve, and open the printed URL with ?mode=enforce. Python 3.14.4 and Chrome 152 were used. The server generates a new random nonce for each document and deliberately leaves one script without it. The loopback reporting receiver accepts and discards report bodies; the displayed evidence comes from browser violation events. Press Control-C when finished.

import base64
import json
import secrets
import sys
from http.server import BaseHTTPRequestHandler, HTTPServer
from threading import Event, Thread
from urllib.parse import parse_qs, urlsplit
from urllib.request import urlopen


class Handler(BaseHTTPRequestHandler):
    def log_message(self, *args):
        pass

    def do_POST(self):
        self.rfile.read(int(self.headers.get("Content-Length", "0")))
        self.send_response(204)
        self.send_header("Content-Length", "0")
        self.end_headers()

    def do_GET(self):
        mode = parse_qs(urlsplit(self.path).query).get("mode", ["enforce"])[0]
        assert mode in {"enforce", "report", "intersect"}
        nonce = base64.b64encode(secrets.token_bytes(18)).decode()
        policy = ("default-src 'none'; script-src 'nonce-" + nonce + "'; "
                  "base-uri 'none'; object-src 'none'; report-uri /reports")
        body = ("""<!doctype html><meta charset="utf-8"><title>CSP nonce lab</title>
<h1>Nonce, report-only, and policy intersection</h1>
<pre id="result">If both scripts are blocked, this text stays unchanged.</pre>
<script nonce=""" + '"' + nonce + '"' + """>
window.allowed=true; window.violations=[];
document.addEventListener('securitypolicyviolation',event=>{
  window.violations.push({directive:event.effectiveDirective,disposition:event.disposition,blocked:event.blockedURI});
});
</script>
<script>window.unapproved=true;</script>
<script nonce=""" + '"' + nonce + '"' + """>
setTimeout(()=>{
  window.labResult={allowed:window.allowed===true,unapproved:window.unapproved===true,violations:window.violations};
  document.getElementById('result').textContent=JSON.stringify(window.labResult,null,2);
},100);
</script>""").encode()
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Cache-Control", "no-store")
        self.send_header("Content-Security-Policy-Report-Only" if mode == "report" else "Content-Security-Policy", policy)
        if mode == "intersect":
            self.send_header("Content-Security-Policy", "script-src 'none'")
        self.end_headers()
        self.wfile.write(body)


server = HTTPServer(("127.0.0.1", 0), Handler)
thread = Thread(target=server.serve_forever, daemon=True)
thread.start()
origin = f"http://127.0.0.1:{server.server_port}"
try:
    policies = []
    for mode in ("enforce", "enforce", "report", "intersect"):
        with urlopen(origin + "/?mode=" + mode, timeout=5) as response:
            key = "Content-Security-Policy-Report-Only" if mode == "report" else "Content-Security-Policy"
            values = response.headers.get_all(key)
            assert len(values) == (2 if mode == "intersect" else 1)
            assert "'nonce-" in values[0]
            policies.append(values[0])
            response.read()
    assert policies[0] != policies[1]
    print(json.dumps({"app_url": origin, "nonce_changes_per_response": True,
                      "wire_policy_checks": "PASS"}), flush=True)
    if "--serve" in sys.argv:
        Event().wait()
except KeyboardInterrupt:
    pass
finally:
    server.shutdown()
    thread.join(timeout=5)
    server.server_close()

In enforce mode, the authorized script sets allowed to true, while unapproved remains false. The relevant violation has effective directive script-src-elem, blocked value inline, and disposition enforce. Change the URL to ?mode=report: both script flags become true, and the violation’s disposition becomes report. Additional resource violations may appear, so identify the relevant event by its fields instead of assuming a fixed total report count.

Finally open ?mode=intersect. A second enforcing header says script-src 'none'; all three script elements are blocked, including those with the matching nonce. The explanatory text stays unchanged, and browser inspection confirms both globals are absent. The Python probe separately verifies changing nonce values and the two header fields. This exercise uses the older report-uri transport for its local sink, not a production reporting service. It establishes nonce, reporting, and intersection behavior for these documents; representative application pages still need their own functional checks before enforcement changes.

Sources