How Chromium's Content Security Policy Mitigates XSS
This article explores how the Chromium web browser implements Content Security Policy (CSP) to defend against Cross-Site Scripting (XSS) attacks. By examining the browser's internal architecture—specifically the Blink rendering engine and V8 JavaScript engine—we will cover how Chromium parses CSP headers, restricts unauthorized resource loading, blocks malicious inline scripts, disables dynamic code evaluation, and enforces Trusted Types to neutralize XSS vulnerabilities before they can execute.
Architecture and Policy Parsing
Chromium enforces CSP primarily within Blink, its rendering engine,
alongside the V8 JavaScript engine. When a web page loads, Chromium's
network stack receives the Content-Security-Policy HTTP
response header (or reads the <meta> tag) and passes
it to Blink.
Blink parses these directives and binds a
ContentSecurityPolicy object directly to the document's
ExecutionContext. Every security-sensitive action the page
takes—from fetching an external file to executing a script—must pass
through this central object for validation. If multiple policies are
defined, Chromium enforces all of them simultaneously, requiring an
action to pass every policy before it is allowed.
Neutralizing Inline Scripts
The most common vector for XSS is the injection of malicious inline
scripts into a page's HTML. By default, when a script-src
or default-src directive is active, Chromium blocks all
inline <script> blocks, inline event handlers (such
as onerror or onload), and
javascript: URLs.
When Blink's HTML parser encounters an inline script or event
handler, it queries the document's ContentSecurityPolicy
object before doing anything else. If the unsafe-inline
directive is not present, Blink refuses to pass the script payload to
the V8 engine for compilation and execution. This immediately stops
reflected and stored XSS attacks that rely on injecting raw script
tags.
Cryptographic
Nonces, Hashes, and strict-dynamic
To allow legitimate inline scripts while still blocking injected XSS payloads, Chromium supports cryptographic nonces (numbers used once) and hashes.
- Nonces: Before executing an inline script, Blink
checks if the
<script>tag contains anonceattribute that matches the random value defined in the server's CSP header. Because an attacker cannot guess this random value, their injected scripts are blocked. To prevent attackers from stealing the nonce via DOM inspection, Chromium hides the attribute value from the DOM API after the page loads. - Hashes: Alternatively, Blink calculates the SHA-256, SHA-384, or SHA-512 hash of a script's exact text content. It only passes the code to V8 if the calculated hash matches an authorized hash listed in the CSP header.
strict-dynamic: Chromium also supports thestrict-dynamickeyword. When active, Blink grants trust to a nonce- or hash-validated script, allowing that specific script to dynamically create and load new scripts via DOM APIs likedocument.createElement('script'). At the same time, it instructs Blink to ignore host-based whitelists, which attackers frequently bypass using open redirects or JSONP endpoints.
Restricting Dynamic Code Execution
XSS attacks frequently rely on converting plain text strings into
executable code using functions like eval(),
setTimeout(), setInterval(), or
new Function(). Chromium mitigates this through a tight
integration between Blink and the V8 engine.
Unless the unsafe-eval keyword is explicitly defined in
the CSP, Blink configures the current V8 context to block dynamic code
generation from strings. When V8 encounters an eval() call
during runtime, it triggers a callback to Blink to verify permissions.
Blink denies the request, and V8 throws an EvalError
exception instead of compiling and running the injected payload.
Controlling External Script Loads
Attackers often attempt to load large, malicious scripts from external domains they control. Chromium prevents this at the resource fetching layer.
Before initiating any network request for a subresource, Blink’s
ResourceFetcher consults the
ContentSecurityPolicy object. If an attacker injects a
<script src="https://attacker.com/xss.js"> tag, Blink
compares the target URL against the origins whitelisted in the
script-src directive. Because the attacker's domain is not
on the allowlist, Blink aborts the request entirely before it ever
reaches Chromium's Network Service.
Defeating DOM-Based XSS with Trusted Types
Traditional CSP directives focus on network requests and HTML
parsing, leaving applications vulnerable to DOM-based XSS, where
client-side JavaScript unsafely manipulates the DOM at runtime. To solve
this, Chromium pioneered the implementation of Trusted Types, which is
activated via the require-trusted-types-for 'script' CSP
directive.
When this directive is parsed, Blink locks down over 60 dangerous DOM
injection sinks, such as Element.innerHTML,
document.write(), and HTMLScriptElement.src.
Instead of accepting raw, potentially malicious strings, Blink requires
developers to pass specialized, type-checked objects (such as
TrustedHTML, TrustedScript, or
TrustedScriptURL). These objects can only be created
through a secure, developer-defined sanitization policy. If an attacker
successfully injects a malicious string into a variable that flows into
a sink like innerHTML, Blink detects that the input is a
raw string rather than a TrustedHTML object, throws a
TypeError, and blocks the DOM modification.