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 a nonce attribute 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 the strict-dynamic keyword. 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 like document.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.