4.1 Browser Security Boundaries: Same-Origin Policy, XSS, and CSP
When a browser opens pages from a bank, an email service, and an unfamiliar site, it must prevent the unfamiliar site from reading data from other domains. Much of web security rests on a single core boundary: the origin.
Origin and Site Are Not the Same Concept
For ordinary HTTP(S) pages, an origin consists of scheme, host, and port; an omitted port uses the protocol default. URLs such as data: can have opaque origins and do not follow this tuple:
https://app.example.com:443Any difference in these components results in a different origin. The same-origin policy restricts scripts from reading responses or DOM elements from cross-origin sources, but allows certain cross-origin requests to be sent, as well as permits a page to load images, styles, and scripts from external sources.
Cookie SameSite uses a site boundary, typically determined by the registrable domain and scheme. a.example.com and b.example.com can be cross-origin while remaining same-site. Therefore, the claims that "the same-origin policy prevents CSRF" and "SameSite can isolate untrusted subdomains" are both invalid.
XSS Runs Attacker Code in a Trusted Origin
Once attacker JavaScript runs within the application's origin, it can read the DOM, Web Storage, and non-HttpOnly cookies, invoke same-origin APIs, modify the interface, and act on behalf of the user. HttpOnly prevents direct script access to that cookie; it neither protects page data nor stops the browser from sending authenticated requests. Common entry points:
- Stored XSS: malicious content is persisted and then displayed to other users;
- Reflected XSS: request parameters enter the response without proper sanitization;
- DOM-based XSS: untrusted data is injected into dangerous DOM sinks on the client side.
The root cause isn't the absence of filtering <script>, but rather treating untrusted data as part of an executable syntax.
Output encoding must match context
The same string must be processed differently depending on its context:
<div>HTML text context</div>
<a href="URL attribute context">link</a>
<script>JavaScript context</script>
<style>CSS context</style>HTML text encoding cannot safely replace JavaScript string, URL, or CSS encoding. URLs also require scheme and destination validation; percent-encoding alone does not make javascript: safe. Prefer letting the template engine automatically escape based on context, and avoid inserting user-provided data directly into inline scripts or styles.
On the browser side, prefer sinks that do not parse HTML:
element.textContent = untrustedTextRather than:
element.innerHTML = untrustedTextIf the product must accept rich text, use a well-maintained HTML sanitizer configured with a clear allowlist, and continuously test for differences between the sanitizer and browser parsing. Regular expressions are not reliable for parsing HTML.
URLs Can Also Serve as Execution Entry Points
Before setting href, src, or redirect targets, URLs must be parsed and constrained by scheme, host, and intended use. javascript:, data:, protocol-relative URLs, user information segments, and URL encoding can all bypass simple prefix checks.
For external links opening in new windows, explicitly use rel="noopener" to prevent the new page from controlling the opener; modern browsers usually provide this implicitly for target="_blank". Redirect targets should use server-side mappings or strict allowlists, never pass arbitrary next parameters directly into Location.
CSP Adds a Layer of Defense
Start by collecting violations with report-only mode, then progressively tighten the policies:
Content-Security-Policy:
default-src 'none';
script-src 'nonce-<per-response-random-value>' 'strict-dynamic';
style-src 'self';
img-src 'self' https: data:;
connect-src 'self' https://api.example.com;
base-uri 'none';
object-src 'none';
frame-ancestors 'none'The header is wrapped for readability; send it as one complete header value. Observation mode uses Content-Security-Policy-Report-Only and does not block violations. Generate at least 128 random bits with a CSPRNG for each response, Base64-encode them, and put the nonce in both the policy and trusted script attributes. Replace the placeholder and keep user input out of nonce-bearing scripts.
In CSP3 implementations, strict-dynamic extends trust to scripts dynamically created by these trusted scripts and ignores host/scheme allowlists, self, and unsafe-inline in the same script-src. A trusted loader must therefore reject arbitrary user-supplied URLs. Policies without this trust mechanism remain vulnerable when broad sources or unsafe-inline allow execution through JSONP, upload directories, or compromised subdomains.
Trusted Types can use require-trusted-types-for 'script' to restrict designated DOM XSS sinks to values created by controlled policies in supporting browsers. It does not sanitize HTML automatically: a policy that passes through arbitrary strings still sends dangerous content to the sink.
Third-Party Scripts Have Page Permissions
Analytics, customer support components, and tag managers typically run within the page's origin and can access the same data as first-party scripts. Subresource Integrity (SRI) can verify the bytes of a fixed cross-origin resource:
<script
src="https://cdn.example/widget.js"
integrity="sha384-..."
crossorigin="anonymous"></script>Replace the digest placeholder with a hash of the actual resource bytes; cross-origin SRI also requires the resource server to allow the read through CORS. Under the earlier CSP, this script additionally needs a nonce or other applicable CSP authorization. SRI does not grant that permission. Frequently updated scripts are difficult to pin, and SRI does not restrict execution permissions. Third-party code requires supply chain reviews, minimal data exposure, and isolation strategies. High-risk components may benefit from sandboxed iframes and narrow messaging protocols.
Verification Checklist
- Where does each untrusted value ultimately end up in the parsing context?
- Does the template default to escaping, and where is escaping explicitly disabled?
- Does the client contain
innerHTML,document.write, or the stringeval, etc., as sinks? - Do regression tests cover rich-text sanitizer bypasses?
- Does the Content Security Policy (CSP) rely on nonces or hashes rather than broad allowlists?
- Are third-party scripts truly required to run in the main origin?
- After a successful XSS, can HttpOnly, re-authentication, and resource-level authorization effectively limit the attack surface?
Next, follow injections through SQL, commands, templates, and paths in server-side input boundaries.