This document describes the fundamental security architecture and assumptions of Chromium, which is called the “security model” of Chromium. It also describes common types of violations of that model, which are attacks against Chromium.
Some parts of this document make reference to V8 as well. V8 can be built as a standalone tool called “d8”, which is useful for finding or demonstrating some kinds of attacks.
Notes for humans: this document is not part of the VRP rules or guidelines; it is advice for AI agents auditing Chromium for bugs.
Chromium is separated into several processes, including the browser process, the GPU process, the network service process, and potentially many renderer and utility processes. Each process except the browser process is inside a “sandbox”, which uses primitives supplied by the OS to constrain what that process can do. The sandbox is different for each type of process. Renderer and utility processes have the strongest sandbox, the network and GPU processes have weaker sandboxes, and the browser process has no sandbox at all.
Communication between processes happens over an IPC system named Mojo. Mojo sends messages between processes, with serialization and deserialization at either end, and can also pass file handles and some other kinds of object between processes. Mojo allows for interprocess calls to methods grouped together into interfaces, and not all interfaces are available to all callers.
Site Isolation is a security architecture that locks some renderer processes to a specific site or origin. The process lock allows the browser processes to restrict the capabilities of a renderer process. Exactly which capabilities are restricted is documented in compromised-renderers.md. These restrictions are designed to hold even if an attacker can run arbitrary native code in a renderer process (after exploiting a separate bug). For more information about Site Isolation please read //content/SECURITY.md.
If compromised-renderers.md documents that a compromised renderer cannot do something (e.g., spoof the Sec-Fetch-Site or Origin HTTP request headers), then bypassing this restriction is a security bug.
If compromised-renderers.md does not document a particular restriction (e.g., it does not prohibit spoofing a Referer HTTP request header), then it is not a security bug if that restriction is not enforced (e.g., if the browser process allows arbitrary Referer header values).
In some cases, omissions in compromised-renderers.md may be themselves considered a security bug. Human judgement is required to evaluate such bugs, because updating the threat model needs to consider various design aspects - like 1) the subjective value of the additional security enforcement, 2) the feasibility of the enforcement (e.g. whether naive process-lock-based checks would block legitimate scenarios), and 3) the engineering and/or performance cost of the enforcement (e.g. the cost of re-architecting things in a way that avoids trusting renderer-provided values). Because of the importance of the human judgement, AI agents should not report such bugs.
The fundamental unit of isolation on the web is the “origin”. A “page” is the contents of a tab, which may be composed of content from multiple origins combined together. In general, scripts or other content from one origin shouldn't affect or see scripts or content from another origin, unless both origins opt into sharing.
Inside Chromium, a WebContents represents a single loaded page, and may contain content from multiple origins which are hosted in separate RenderFrames. RenderFrames that are in different origins are usually in separate renderer processes, isolated from each other.
When discussing attacks across origins, we sometimes use the example origins “evil.com” to mean an attacker's origin, and “good.com” to mean an origin they are trying to steal or corrupt data from. In this context, both origins are generally assumed to be loaded in Chromium at the same time, but in separate frames.
Chromium exposes several ways for websites to run their own code on the user‘s machine. The two most common are Javascript and Webassembly. V8 is responsible for handling both of these, and V8 should ensure that code supplied by websites isn’t able to have dangerous effects inside the renderer process, like corrupting memory or causing a use-after-free of an object.
V8 compiles Javascript or Webassembly into native code to be executed by the CPU. V8 must guarantee that that native code it generates doesn‘t break certain safety properties, such as being unable to overwrite data structures that aren’t meant to be exposed to Javascript.
There are a few different possible build configurations of Chromium or of V8. The three that are most relevant for security purposes are:
is_asan=false is_debug=false in args.gnis_asan=false is_debug=true in args.gnis_asan=true is_debug=false in args.gnThere are several different ways that invariants in Chromium and V8 are enforced. The CHECK macro enforces an invariant even in released builds, and the DCHECK macro enforces an invariant in debug builds only. Some kinds of invariant violation, like indexing outside the bounds of a base::span, are also detected in release builds and cause an immediate crash.
Every attack comes with a series of steps, which are generally supplied as a short program, called a “proof of concept”. A “proof of concept” provides evidence that a bug is present, and is not the same thing as an “exploit”, which makes use of a bug to achieve a malicious goal. A “proof of concept” is by definition harmless, but should have some objective and verifiable external effects to demonstrate that the bug is present.
A security bug report (or vulnerability report) is a description of how to execute an attack. Every security bug report must take the form: “An attacker can cause Chromium to [something which harms the user] by [attack steps].” For example:
window.foo(12345).foo().foo attribute set to bar.Unless you can clearly describe both the security harm to the user, and the attack steps, you do not have a security bug report.
A security bug that allows for execution of arbitrary code of an attacker's choice outside of any sandbox (so in the browser process or elsewhere in the OS outside of Chromium) or in a sandbox weaker than the renderer sandbox (some target process) is the most severe category of attack. These bugs generally take two forms:
For this kind of bug, the proof of concept must either:
To simulate a compromised renderer process, use the MojoJS feature by passing --enable-features=MojoJS to Chromium. This allows you to call Mojo methods from JavaScript. If that does not work for some reason, the proof of concept may be in the form of a patch intended to apply to the Chromium source tree. If so, this patch must modify code that is only executed in the renderer, to simulate a compromised renderer process.
Anything that does not lead to one of these consequences is almost certainly not a valid proof of concept for an escalated code execution bug.
A security bug that allows for execution of arbitrary code of an attacker's choice inside the renderer sandbox is a renderer code execution bug. These bugs generally take two forms:
For V8, code compiled from attacker-supplied Javascript or Webassembly is executed inside a “heap sandbox”, which confines memory reads and writes performed by that compiled code to a certain region. Bugs which allow for reading or writing memory outside the heap sandbox are “heap sandbox escapes” and are very valuable bugs. Bugs which allow for reading or writing memory inside the heap sandbox are less valuable but still valuable.
For this kind of bug, the proof of concept must either:
Not all DCHECK failures in V8 or d8 are security bugs, but many are, so to be conservative we assume that all of them are security bugs until we are confident they are not.
Any proof of concept that uses d8 must use --run-as-security-poc, in addition to any other command-line arguments it needs. Proofs of concept that do not work with --run-as-security-poc are not security bugs.
A security bug that allows for one origin to either read data from another origin or run code in another origin is a cross-site bug. There are some intentional ways to share data between origins, but by default origins shouldn't be able to interact.
A proof of concept of a cross-site bug must show that:
A bug which allows an extension to take actions it shouldn't be able to is an extension security bug. Extensions have permissions defined in their manifest.json, and all extensions have certain implicit permissions like the ability to make network requests in their own context.
For this kind of bug, the proof of concept is an extension which, when loaded, takes an action the extension should not be able to take.
Any other bug which allows a website to cause Chromium to harm a user may be a security bug, even if it doesn't fit one of these categories. Any such bug needs a clear explanation of the security consequences of the bug, as well as who can exploit it. Such a bug also needs clear reproduction steps or an executable proof of concept.
There are some common kinds of reports which are not security bugs in Chromium:
--single-processunsafe--run-as-security-poc added to the command line.These are not security bugs, regardless of how easy they are to trigger. Under no circumstances is something matching an entry in this list a security bug.
When reporting a bug:
For bugs that reproduce via d8, a single Javascript source file called “poc.js”
For bugs that reproduce via loading in Chromium, a single HTML source file called “poc.html”
For bugs that reproduce via loading in Chromium, but which cannot be served as a single static HTML file (for example, when custom HTTP headers or dynamic responses are required), provide both “poc.html” and a Python HTTP server called “server.py”. The reproduction environment places poc.html and server.py in a directory containing a gen symlink to the build's generated files, then runs:
python3 server.pychrome http://localhost:8000/poc.html Therefore, server.py must:poc.html at http://localhost:8000/poc.html.poc.html and the Mojo bindings in gen from disk, serving the bindings unmodified under the /gen/ URL path. All client-side HTML and JavaScript payloads should live in poc.html (or be generated dynamically by server.py).The payload in poc.html imports bindings through ./gen/:
import {FooRemote} from './gen/path/to/foo.mojom.m.js';
Because server.py serves its own working directory, both /poc.html and /gen/ resolve directly through SimpleHTTPRequestHandler:
import http.server, os os.chdir(os.path.dirname(os.path.abspath(__file__))) # poc.html and ./gen live here class Handler(http.server.SimpleHTTPRequestHandler): def end_headers(self): # Add any custom headers required by the exploit, e.g.: # self.send_header('Cross-Origin-Opener-Policy', 'same-origin') # self.send_header('Cross-Origin-Embedder-Policy', 'require-corp') super().end_headers() http.server.ThreadingHTTPServer(('', 8000), Handler).serve_forever()
To run the PoC locally, create the same symlink next to server.py and poc.html: ln -s <chromium-out-dir>/gen gen.
For bugs that simulate a compromised renderer with a source patch, a single unified diff called “poc.patch”. This patch must only change code that runs in the renderer.
For bugs that have a demonstration video, a single mp4 file called “poc.mp4”.
In general, a shorter bug report is better. Bug reports should be as short as possible, but no shorter.
The docs/security directory contains other security-related documentation. You should also consult the README.md and SECURITY.md files for the directories you are looking for bugs in, if they are present.