Content Security Policy Deployment and Debugging

Content Security Policy lets a server tell a browser which classes of content and execution it permits. A useful deployment combines a policy suited to the application, observed legitimate workflows and tests showing that a representative unwanted execution is actually blocked.

Report-only mode helps discover violations without enforcing that candidate policy. It is an observation stage, not the point at which the new policy protects users. Preserve any existing enforced policy while evaluating a revision, then verify enforcement independently.

TL;DR

Quick Example

This complete Python 3.12 teaching server binds only to loopback. Save it as csp_demo.py, run it, and open the two printed paths in a browser. It compares a trusted nonce-bearing script with a harmless unnonced marker. The local report-uri endpoint discards bounded fixture report bodies; it is not a production reporting service.

In the actual local browser, /report-only showed both scripts ran and Script violation: report. /enforce showed the nonce script ran, the unnonced probe had not run, and Script violation: enforce. Three separately inspected HTTP responses had different matching header/body nonces. Stop the server with Ctrl-C. This tests only these loopback pages, not a real application, every browser, report aggregation or a complete XSS defense.

Core Concepts

Policy delivery and composition

An enforced Content-Security-Policy response header and a Content-Security-Policy-Report-Only header have different effects. They can coexist: the former still restricts behavior while the latter evaluates a candidate. A report-only policy cannot be delivered through a meta element. Review all effective policies rather than assuming the last visible header replaces earlier restrictions. CSP policy delivery

Nonces grant execution authority

A nonce must be unpredictable and fresh for each response. The server places it in the policy and only on scripts it intends to authorize. Automatically adding the nonce to every script-shaped fragment can authorize injected content too. A trusted script can still perform unsafe operations; the nonce does not make its logic safe. Nonce design

Defense in depth

OWASP frames CSP as an additional layer, especially against script injection. It does not remove injection bugs, replace context-aware encoding/sanitization, or enforce application authorization. Review the vulnerability and the browser policy as related but separate controls. OWASP CSP guidance

Roll out from observed workflows

Inventory scripts, styles, images, network destinations, workers, frames and other capabilities the application uses. Trace how those resources are loaded in the actual build and response path. Include authenticated and error pages, lazy routes, third-party widgets and any supported embedded contexts.

Keep the candidate narrow enough to understand. If a legitimate workflow violates it, identify the exact load or execution pattern before changing the policy. Some failures should be repaired in application code or dependency configuration rather than accommodated with broader permissions.

Avoid solving an inline-script violation by reflexively adding unsafe-inline, or an evaluation violation by adding unsafe-eval. Those permissions change the execution boundary. If an unavoidable legacy requirement remains, document its scope and review an appropriate migration plan rather than presenting the relaxed policy as equivalent to the original.

Stage enforcement on representative traffic or an approved test environment and monitor both functionality and security behavior. Define who owns violations and what event stops the rollout. A clean report window can mean missing traffic, unsupported collection or filtered diagnostics; it does not prove the policy is complete.

Debug from the directive and execution path

Record the effective directive, blocked resource category, disposition, response/build identity and the user journey needed to reproduce the issue. Use browser diagnostics and server/header evidence together. A policy string in source code may differ from what a CDN, reverse proxy or error response actually delivers.

Check nonce plumbing across headers, server-rendered markup, caches and dynamically generated responses. A cached header paired with a different body nonce can break valid scripts. Reusing one nonce across many responses weakens the intended design. The fixture uses no-store to keep that teaching boundary simple; production caching needs an explicit compatible design.

If the application uses hashes or delegated script trust, review those semantics under the intended browsers and loaders. A broad hostname allowlist can expose capabilities through trusted origins or script-loading gadgets; merely replacing one long allowlist with another is not a demonstrated injection defense.

Use harmless negative probes in an authorized environment. Confirm that an untrusted script without the required authority cannot set an observable marker, while the intended script still performs its legitimate action. Also test the actual injection/sink category relevant to the application; one blocked inline script does not cover every browser attack path.

Keep frame restrictions, base URL restrictions, object loading and network destinations visible where relevant. They serve different purposes from script-src. Adding a directive to a fixture does not mean each directive was exercised by that fixture.

Make reporting useful without making it authoritative

MDN recommends the Reporting API using Reporting-Endpoints and report-to for CSP violation collection. The teaching server uses a small compatibility report-uri receiver only to support the local demonstration; production reporting design and browser delivery were not validated. Violation reporting

Treat report submissions as untrusted data. Bound body size, validate the accepted schema, rate-limit or aggregate as appropriate and protect access to diagnostic records. URLs and optional code samples can contain sensitive context. Collect only the detail needed for investigation and apply the appropriate retention policy.

Group reports by useful dimensions such as directive, application route/build and known resource category. Keep extension-generated noise and missing-report uncertainty distinguishable from application regressions. Do not automatically convert submitted source URLs into policy permissions.

For rollback, restore a reviewed policy/application combination and verify its actual headers and allowed/denied cases. A policy change cannot retract script execution or data already sent. Preserve the intended baseline protection while resolving a new rollout problem.

Best Practices

Verify headers at the browser boundary

Inspect real responses across normal, authenticated and error paths. Check the delivered header/body nonce relationship rather than source configuration alone.

Pair positive and negative tests

Prove an intended workflow runs and a representative unwanted action does not. Keep the test scoped to the actual execution path exercised.

Separate reports from permissions

Investigate a report’s origin and meaning. A submitted violation is neither a trusted instruction nor a reason to allow the reported resource.

Common Mistakes

Bad: Calling a report-only rollout protective enforcement.

Correct: Verify the candidate under an enforced header while retaining appropriate existing protection.

Bad: Applying a nonce to every script fragment after rendering.

Correct: Authorize only intended script elements and review untrusted markup/sinks separately.

Bad: Automatically allowing every reported source.

Correct: Reproduce the violation and decide whether the code path or policy should change.

FAQ

Does a nonce make a trusted script harmless?

No. It authorizes execution under the policy. The script’s data handling, DOM sinks and delegated loads still need review.

Does a clean report dashboard prove CSP works?

No. Check traffic coverage, collection and effective responses, then perform meaningful positive/negative enforcement tests.

What did the example actually prove?

In the tested local browser, the chosen nonce script ran and the unnonced marker was blocked only under enforcement. It did not evaluate a real site or every injection category.

Related Topics

References