Content Security Policy (CSP) constrains what a page may load and execute, reducing the impact of script injection, malicious resource loading, and unwanted framing. It is not a security header that can be completed by copying the strictest-looking example. A production page also has analytics, fonts, images, APIs, WebSockets, payment widgets, and old inline code. Turning on enforcement in one step often blocks legitimate behavior first.

A deployable CSP is a resource-execution contract: discover what the page needs, observe deviations, migrate dangerous patterns, and then enforce the boundary in stages.

Draw the resource graph before writing an allowlist

Use browser network records, templates, and build output to inventory scripts, styles, images, fonts, connections, frames, workers, media, and form targets. Give every item an origin, business owner, reason, and removal condition. A third-party hostname is not permanent permission. If nobody can explain why a source exists, remove it before granting access.

default-src is only a fallback for some fetch directives; it does not define every security boundary. Directives such as base-uri, frame-ancestors, and form-action need explicit decisions. A same-origin application might begin with this candidate:

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' 'nonce-{PER_RESPONSE_RANDOM}';
  style-src 'self';
  img-src 'self' data:;
  connect-src 'self' https://api.example.com;
  object-src 'none';
  base-uri 'none';
  frame-ancestors 'none';
  form-action 'self';
  report-to csp-endpoint

This is a hypothesis to test, not a universal answer. Products that embed content, are embedded, use a CDN, or submit to an external identity provider must model those cases deliberately. Do not add *, https:, or 'unsafe-inline' merely to silence reports.

Report-Only is a migration tool, not the finished state

Deploy the candidate in a Content-Security-Policy-Report-Only response header first. The browser keeps loading resources but records violations in developer tools and can send them through report-to to an endpoint declared by Reporting-Endpoints. During a compatibility transition, a site may also send the older report-uri, while recognizing that it has been superseded.

Never convert reports directly into an allowlist. Extensions, injected browser tools, scanners, and real attack attempts all create noise. Limit body size and request rate, parse every report as untrusted input, and aggregate by policy version, page, effective directive, blocked origin, and browser. If report-sample is enabled, samples can contain user data; reporting storage therefore needs access controls, redaction, and a retention policy.

Reports do not prove that a policy is safe. They only describe routes that somebody exercised. Actively test sign-in, checkout, error pages, lazy loading, printing, and low-traffic administration paths across supported browsers.

Use nonces for dynamic pages and hashes or external files for static pages

A nonce must be generated by a cryptographically secure source for every response. CSP Level 3 recommends at least 128 bits before encoding. The 'nonce-…' in the response header must match the nonce attribute on each trusted script:

<script nonce="" src="/assets/app.js"></script>

Never reuse a fixed nonce or run middleware that automatically adds one to every script tag; that can authorize attacker-injected tags too. A template should attach the nonce only to nodes the application owns. base-uri 'none' also prevents an injected base element from redirecting a trusted relative script URL.

A purely static site cannot generate a per-response nonce. The simplest design is to remove inline scripts and load same-origin files. If a stable inline fragment is essential, put a SHA-256 or stronger hash of its exact contents in the policy. Whitespace and build transformations change that hash, so calculate it in the build and update policy and content together.

'strict-dynamic' lets a nonce- or hash-authorized script extend trust to non-parser-inserted scripts that it creates. That helps some module loaders, but it increases the responsibility of trusted code. If trusted JavaScript passes an unvalidated URL into a dynamic script API, an attacker may still gain execution. Audit every dynamic script creation site before adopting it.

Tighten layers without waiting for “zero reports”

Start by enforcing low-risk, well-understood boundaries such as object-src 'none', base-uri, frame-ancestors, and form-action, while a stricter script policy remains Report-Only. Remove inline event handlers and eval(), then enable script enforcement by route or traffic cohort. A looser enforced policy and a stricter report-only policy can operate together.

Give each change a policy version, owner, and exact rollback value. Monitor resource-load failures, front-end exceptions, critical-flow completion, and violation trends. Rollback should restore the last verified policy, not add 'unsafe-inline' in an emergency. Once the candidate is stable, move the same value into the Content-Security-Policy response header. Report-Only can then preview the next tightening step.

A meta policy on static hosting has hard limits

When response headers cannot be controlled, a document can put <meta http-equiv="Content-Security-Policy"> near the start of its head. It affects only content that follows it. A meta element cannot deliver Report-Only and does not support report-uri, frame-ancestors, or sandbox. It is therefore a limited fallback, not equivalent to full deployment. Use a CDN, reverse proxy, or hosting configuration when the site needs anti-framing, centralized reporting, and staged rollout.

Put the policy into regression tests

CI can detect new inline event handlers, eval(), dangerous schemes, and unregistered third-party origins. Browser tests must prove both sides of the contract: real routes, lazy loads, and forms still work; injected inline scripts, external scripts, and frames are actually rejected. Read the final response headers during tests too. An application, proxy, and CDN can each add a policy, and browsers enforce all of them together.

Before release, verify that the resource graph has owners; Report-Only produced evidence from real traffic; the reporting endpoint is rate-limited and privacy-aware; nonces are unique per response or static hashes are reproducible; unsafe fallbacks were not added just to quiet alerts; critical paths passed cross-browser tests; and enforcement can be canaried and restored.

CSP is defense in depth, not a substitute for output encoding, trusted templates, or safe input handling. Its value is not the length of the policy string. Its value is a clear boundary around what the browser may execute—and a process that can observe, test, and change that boundary without turning a security improvement into an outage.