Skip to content
Tyrone Software
Web Development

Rolling out a Content-Security-Policy without breakage

Abstract illustration for the article Rolling out a Content-Security-Policy without breakage

Turning on a Content-Security-Policy in enforcing mode on the first try is how checkout pages end up blank on a Friday. The header has a mode built for the alternative.

Report first

Content-Security-Policy-Report-Only uses the same syntax but blocks nothing. The browser sends a JSON report for every violation instead. Send a deliberately strict policy in report-only mode and read the reports for a week or two. You will find scripts nobody remembered: a tag manager loading another tag manager, an A/B tool, an old chat widget.

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' 'nonce-r4nd0m';
  object-src 'none';
  base-uri 'none';
  report-to csp

Expect noise. Browser extensions inject scripts and trigger reports, so filter by source-file and look for patterns instead of chasing every line.

Nonces or hashes

An inline script is allowed if it carries a nonce that matches the header, or if its content hash is listed. Nonces suit server-rendered pages: generate a fresh, unpredictable value per response and put it on each <script>. A nonce that repeats between responses gives no protection. Hashes suit static pages, because the content never changes.

'strict-dynamic' is worth a look. It lets a trusted script (one with a valid nonce) load further scripts, which is how tag managers usually work, without listing every host.

Four step rollout of a Content-Security-Policy from report-only to enforcing
Keep reporting turned on after enforcing. New violations are the early warning.

Inline styles are the smaller risk

Blocking inline scripts removes the most dangerous injection vector. style-src 'unsafe-inline' is a weaker hole, since injected CSS can leak data or restyle the page but cannot run code by itself. Many teams accept it, at first, in exchange for a policy they can actually deploy. Removing inline style attributes entirely is a later, separate project.

Finish with the parts that cost nothing: object-src 'none', base-uri 'none' (or 'self'), and frame-ancestors in place of the old X-Frame-Options.

report-to is replacing report-uri, but keep both for now

The report-uri directive, which points violation reports at a URL directly, is deprecated in favour of report-to, which names a reporting group defined by a separate Reporting-Endpoints (or the older Report-To) header. The newer mechanism is part of the general Reporting API and can also carry other kinds of reports, such as deprecation warnings, through the same pipeline. Because browser support for the newer header rolled out gradually, most real deployments send both directives for a while, with report-uri as the fallback for anything that has not caught up.

Reporting-Endpoints: csp="https://example.com/csp-reports"
Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-r4nd0m';
  report-to csp;
  report-uri /csp-reports

Once the enforced policy is stable, require-trusted-types-for 'script' is worth a look as a follow-up project. It blocks the DOM sinks (innerHTML and similar) that most DOM-based XSS goes through, by requiring that any string assigned to them first pass through a Trusted Types policy your code defines. It needs real code changes in most existing applications, so it belongs after the CSP rollout, not inside it.

Keep reading

Technologies

  • Python
  • PHP
  • Laravel
  • Django
  • PostgreSQL
  • MySQL
  • HTML5
  • CSS3
  • Bootstrap