docs: document preload code cache threat model (#54654)

Explain why SetFromRenderer stores the renderer-produced blob unverified and that the (BrowserContext, ProcessLock, id) scope is the security boundary.


Claude-Session: https://claude.ai/code/session_018A4Mt59Nr4h4WSFjyNNkxb

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
claude[bot]andClaude authored and GitHub committed 2026-10-07 00:47:31 +09:00
1 parent 6b48d9813b
commit cba5248898
2 files changed
+18 -1

No files matched your search

+15
View File
@@ -254,6 +254,21 @@ void SetFromRenderer(content::RenderFrameHost* rfh,
// first launch) just writes identical bytes twice; harmless and rare, and
// SetFromRenderer() is UI-thread serialized so it's not a data race.
Entry entry;
// Security note: |blob| is renderer-produced and stored without
// verification. This is intentional and matches Chromium's
// GeneratedCodeCache (content/browser/renderer_host/code_cache_host_impl.cc).
// The served-preload and source hash checks above only decide whether this
// write is accepted and which source hash it is filed under. They do not,
// and cannot, prove |blob| was compiled from that source (V8's CachedData
// source check is length-only). Containment comes from the key instead:
// entries are scoped to (BrowserContext, ProcessLock, preload id), with the
// lock taken from the browser-side RenderProcessHost (see ScopeForFrame()),
// so a write is only ever served back to frames in the same BrowserContext
// whose process has an equal ProcessLock. A compromised renderer can
// therefore only replace bytecode for its own principal's sandboxed preload,
// whose capabilities it already holds. Persistence across restarts is the
// accepted cost, as in Chromium. Serving an entry across a different
// BrowserContext or ProcessLock WOULD be a security bug.
entry.source_hash = hash_it->second;
entry.blob.assign(blob.begin(), blob.end());
+3 -1
View File
@@ -35,7 +35,9 @@ std::string IdForWebPreferencesPreload(const base::FilePath& path);
// origin keying, sandboxing, guest and cross-origin-isolation status,
// storage partition). Entries are only ever served back within an equal
// scope, so a blob produced by one principal's renderer is never handed to
// another. |dir| is empty for off-the-record contexts (memory tier only).
// another. Scope is the security boundary for cached blobs; see the note in
// SetFromRenderer(). |dir| is empty for off-the-record contexts (memory tier
// only).
struct Scope {
Scope();
Scope(const Scope&);