Custom Software · Security · Production

We shipped a strict CSP to a live SME platform and broke checkout for 6 hours

Published · Updated

Hardening a server-rendered platform with Content-Security-Policy looked like a one-line header change. It took down payment flows in three browsers and taught us a rollout pattern we now reuse.

Security hardening on a server-rendered platform sounds like a checklist: add the headers, run the scanner, ship. Last year we rolled out a strict Content-Security-Policy on a client's booking platform, roughly 40k monthly sessions, and within an hour their payment iframe stopped rendering in Safari and two older Chromium builds. No errors in our logs, because the browser blocks it client-side. We found out from the client's support inbox.

Why CSP breaks things silently

CSP failures are invisible to server-side monitoring. The browser refuses to load a script or frame and reports it, but only if you have a report-uri or report-to endpoint collecting those reports. We had neither. The checkout iframe was blocked by frame-ancestors and script-src at once, and because the payment provider injects inline scripts, our script-src 'self' rule killed the whole flow. Server logs showed a perfectly healthy 200 on every page.

The report-only phase we skipped

The fix we should have started with: Content-Security-Policy-Report-Only. Same header syntax, zero enforcement, browser just POSTs violation reports. We now run report-only for a minimum of two weeks on any platform with real traffic, collecting reports into a dedicated endpoint that dedupes by directive and blocked URI. On the booking platform, two weeks of reports surfaced 14 distinct violations: three legacy inline event handlers, a forgotten Google Fonts preload, and a chat widget loading from four different CDN hosts.

Triage: allowlist, nonce, or delete

Every violation gets one of three decisions. Allowlist the host if it is a maintained third party you trust, which covered the payment provider and fonts. Move inline scripts to external files with a nonce when the code is yours, which handled the legacy handlers in about a day of work. Delete it if nobody remembers why it exists, which was the answer for two tracking pixels from a 2021 marketing campaign. Roughly a third of violations we have triaged across projects end in deletion, not accommodation.

The numbers after enforcement

After switching from report-only to enforced, we kept the reporting endpoint live. First week of enforcement: 2,300 reports, almost all from browser extensions injecting scripts, which you can safely ignore once you recognise the patterns. Legitimate violations dropped to under ten per week by week three. The client's next penetration test flagged zero header findings, down from six medium-severity items the year before. Total effort: about five engineer-days spread over a month, most of it waiting for reports.

What we do differently now

CSP is never a launch-day change for us anymore. It is a three-phase project: two weeks report-only, triage and fix, then enforce with reporting kept on permanently. We apply the same pattern to other silent-failure headers like Permissions-Policy. The trade-off is honest: you spend a month on something users never see, and the payoff is the absence of an incident nobody can prove would have happened.

Takeaway

If you are hardening a live platform, treat CSP as a measurement exercise before it is an enforcement exercise. Stand up a report endpoint, run report-only for at least two weeks of real traffic, and expect a third of what you find to be dead code you should delete rather than accommodate. The header itself is the easy part; the discipline is in the rollout.

Working on a project where these methods apply?