Technical Publication

6. CSP

Content Security Policy Is Organizational Technical Debt

Every other mechanism in this series can, in principle, be set correctly once and left alone. CSP can't — it's the one boundary in the series that decays by construction, on a clock set by how fast the application changes underneath it.

Content Security Policy is a response header — or, less robustly, a <meta> tag — through which a page tells the browser exactly which sources of content it's allowed to load and execute, directive by directive: script-src for JavaScript, style-src for CSS, img-src for images, connect-src for fetch/XMLHttpRequest/WebSocket targets, frame-ancestors for who may frame the page (the mechanism the previous essay depends on), and several more. Absent a policy, a page's HTML and JavaScript can load and execute a script from anywhere, load a stylesheet from anywhere, open a connection to anywhere — the browser enforces almost none of the restrictions here by default. CSP is a default-deny allowlist layered on top of that otherwise-unrestricted behavior: declare what's actually legitimate, and the browser refuses everything not on the list, regardless of how it got into the page's markup.

The reason this exists at all is that it's the most direct defense-in-depth available against the most damaging consequence of cross-site scripting. An XSS vulnerability — attacker-controlled input rendered into a page without adequate escaping — succeeds by getting the browser to execute a script the page's author never intended. If that page has no CSP, an injected <script> tag simply runs, with the full authority of the page's own origin: reading cookies, making authenticated requests, exfiltrating whatever the injected code can reach. A well-constructed CSP breaks that chain at the execution step rather than the injection step: even if the attacker's markup makes it into the page, script-src that doesn't include 'unsafe-inline' and doesn't allowlist the attacker's origin means the injected script simply doesn't run. The browser refuses to execute it, full stop, regardless of how it got there. That's the value proposition — CSP doesn't prevent injection; it neutralizes what injection can do once it happens, which is precisely why it's treated as defense-in-depth rather than a primary fix (escaping and sanitizing untrusted input at render time remains the actual fix for XSS; CSP is the backstop for when that fix has a gap somewhere).

Reading a Policy: What Each Piece Is Actually Deciding

A real CSP header is a semicolon-separated list of directives, each scoping a different category of resource, and reading one closely is a good way to see exactly how much continuous decision-making is compressed into what looks like a single configuration line.
DirectiveWhat it gatesWhere staleness shows up first
script-srcWhich origins may serve executable JavaScript, and whether inline or eval-based execution is allowedA new analytics or A/B testing snippet added without updating the allowlist — the script silently fails to run
style-srcWhich origins may serve CSS, and whether inline styles are allowedA new component library or font-loading pattern that injects <style> tags directly
connect-srcWhich origins fetch, XMLHttpRequest, and WebSocket connections may targetA new API endpoint, webhook receiver, or real-time service added on a different subdomain or vendor domain
img-srcWhich origins images may load fromA new CDN, user-avatar host, or third-party image embed
frame-ancestorsWhich origins may embed this page in a frameA new partner integration that needs to embed the page, or a removed one that no longer should
report-uri / report-toWhere the browser sends a report when something violates the policyWhether anyone is still reading what gets sent there
Every row in that table is a place where the policy can be correct today and wrong the moment a different team, working on a different feature, adds a resource the policy's author had no way to anticipate. That's the mechanism, made concrete: CSP isn't one decision, it's dozens of small, ongoing decisions bundled into a single header, each with its own independent clock for going stale.

The Mechanism That Makes Staleness the Default Outcome, Not a Failure

Here's what separates CSP from the other five mechanisms in this series, and it's worth being precise about the distinction rather than asserting it: SSRF, CSRF, SameSite, CORS, and clickjacking are all boundaries that can be set correctly and then, in principle, remain correct — nothing about the boundary itself demands ongoing revision unless the surrounding system changes in a way that specifically invalidates it. CSP is different by construction. A correct CSP is not a static fact about an application; it is a live, exhaustive inventory of every legitimate origin the page loads content from and every inline-execution pattern it genuinely needs — and that inventory changes every time a team adds an analytics tag, a new CDN, a payment widget, a chat embed, a font provider, an A/B testing script, an ad network, a monitoring SDK. Each of those additions requires a corresponding addition to the policy, or the new feature breaks silently (the resource gets blocked, not loaded) the moment the policy is enforced rather than merely reported.

That dynamic produces a predictable organizational response, and it's the one worth naming directly: teams that don't want to spend attention on shepherding the allowlist tend to converge on wildcards, or on broad 'unsafe-inline'/'unsafe-eval' allowances, or on not shipping an enforced policy at all and leaving Content-Security-Policy-Report-Only running indefinitely — a mode that logs violations without blocking anything, which is genuinely useful for building a policy safely, and also an easy place to get permanently stuck, since a report-only policy protects nothing while giving the appearance of having addressed the problem.

Enforcement Mode, Report-Only Mode, and the Gap Between Them

CSP ships in two operating modes, delivered via two different headers, and the distinction between them is where a lot of the maintenance burden described above either gets managed responsibly or quietly deferred indefinitely. Content-Security-Policy is the enforcing header — the browser actually blocks anything that violates the policy. Content-Security-Policy-Report-Only describes the same policy syntax but changes nothing about what the page can load; it only sends a report, to whatever endpoint report-uri or the newer report-to names, describing what would have been blocked had the policy been enforced. The intended workflow is to run report-only first, against real production traffic, watch the violation reports accumulate, adjust the policy until legitimate resources stop generating violations, and only then flip to enforcing mode.

That workflow depends entirely on someone actually reading the reports and doing the adjustment work before flipping the switch — and because report-only mode blocks nothing, there's no forcing function that makes skipping that step visible. A team can ship a report-only policy, generate a dashboard nobody checks, and carry that configuration for years, technically "having CSP" in the sense that a header is present, while providing zero actual protection, because the enforcing header was never turned on. This is a specific, common version of the general pattern the rest of this essay describes: a control that looks, from the outside — a header exists, a policy is defined — like it's doing its job, while the step that would make it actually do that job was never assigned to anyone as something to finish.

The Payoff: This Is the Thesis, Stated Plainly

This is the essay in the series where it's worth saying the underlying claim outright, rather than letting it stay implicit, because CSP is the mechanism where the claim is closest to the surface. Every other essay here has traced a version of the same story — a trust decision made once, implicitly, by whoever was solving an adjacent problem at the time, inherited by everyone downstream, never assigned to a specific owner responsible for checking whether it still held. SSRF: a network boundary set by infrastructure, consumed by application code that never knew the boundary existed. CSRF: a credential-attachment default set by the original cookie mechanism, inherited by every session-based application afterward. SameSite: a browser vendor correcting that default years after the harm had already accumulated industry-wide. CORS: a header written once to unblock a frontend integration, never revisited as new origins accumulated against it. Clickjacking: a boundary that belongs to whoever composites the final screen, with no lever at all over sites that never had it to lose.

CSP is the case where the mismatch between "set once" and "needs continuous maintenance" isn't a subtle organizational dynamic that takes some argument to surface — it's the literal, mechanical shape of the control. A CSP doesn't merely risk drifting out of date the way a CORS policy might if nobody happens to revisit it. It drifts out of date on a schedule set by the ordinary pace of adding features to a web application, which is to say: constantly, by default, unless someone is specifically doing the work of keeping it current. That's not a security control that failed. That's a security control whose entire value proposition depends on an ongoing organizational commitment that most organizations don't assign to anyone, structured no differently than unpatched dependencies, undocumented tribal knowledge, or any other form of technical debt that accrues quietly because maintaining it was never anyone's explicit job — except that here, the debt is denominated in exactly the kind of exposure the six mechanisms in this series exist to describe.

The organizational lesson underneath all six essays is the same lesson, seen from six technically unrelated angles: a system complex enough to have implicit trust boundaries will always have some of them drift out of calibration, because staying calibrated requires continuous attention, and neither browsers, servers, nor the internal politics of who's responsible for what assign that attention automatically. Somebody has to decide, deliberately, who owns each boundary — and the honest, current answer, for most of these six, on most systems, is still nobody.

The trust boundary here is between the set of resources and scripts an application actually loads today and the policy still describing what it loaded when someone last updated it — and on most applications still actively shipping features, the answer to who owns keeping those two in sync is whoever happened to touch the header last, which is a person, not a role, and people move on to other work.