CVE-2026-87553
Overview
Background
- Potentially-trustworthy origin
- an origin the browser treats as a secure context (gating powerful web features like geolocation, service workers, and some permissions), which for an opaque origin depends on whether its precursor is secure.
- Opaque origin precursor
- an opaque/unique origin records the tuple origin it derived from, retrievable via
url::Origin::GetTupleOrPrecursorTupleIfOpaque, so an opaque document nested in anhttpspage can still be judged trustworthy. - `SetLastCommittedOrigin`
- the browser-process
RenderFrameHostImplmethod that records a frame’s committed origin and its “is potentially trustworthy unique origin” bit into theFrameReplicationState/ browsing-context state. - `DidCommitProvisionalLoadParams` (`navigation_client.mojom`)
- the IPC payload the renderer sends the browser at navigation commit; any field in it is attacker-controlled once the renderer is compromised.
Root Cause Analysis
Before the patch, the has_potentially_trustworthy_unique_origin bit was computed in the renderer (frame_origin.IsOpaque() && frame_origin.IsPotentiallyTrustworthy()), shipped to the browser inside the commit IPC struct in navigation_client.mojom, and then merely clamped and forwarded by SetLastCommittedOrigin into the FrameReplicationState. This violated the SiteIsolation invariant that security-relevant properties of an origin must be derived from browser-authoritative data, not asserted by the renderer: a compromised renderer could set the bit true for an opaque origin whose precursor is not secure, making the browser treat a non-secure opaque document as a secure context.
The fix removes the field from the mojom struct and from both the renderer’s params builder and the browser’s SetLastCommittedOrigin signature, and instead recomputes the bit inside the browser from the origin itself — origin.opaque() && precursor.IsValid() && network::IsUrlPotentiallyTrustworthy(precursor.GetURL()). Because the value now comes from the browser’s own url::Origin and the network trustworthiness check, the renderer can no longer forge it. The change is a hardening/removal of renderer-supplied trust input, not a relaxation of any check.
Attack Path
- Compromise a renderer The attacker gains arbitrary control of a renderer process (the standard SiteIsolation threat model), letting it forge the contents of the commit IPC.
- Forge the trustworthy bit on commit
In the
DidCommitProvisionalLoadParamssent for an opaque-origin document, the renderer setshas_potentially_trustworthy_unique_origin = trueeven though the origin’s precursor is not actually potentially trustworthy. - Browser records the forged bit
SetLastCommittedOriginaccepts the renderer value (only clamped, not recomputed) and stores it in the frame’sFrameReplicationStateviabrowsing_context_state()->SetCurrentOrigin. - Secure-context decisions are subverted Downstream secure-context / powerful-feature gating keyed on that bit treats a non-trustworthy opaque document as trustworthy (inferred), granting it access normally reserved for secure contexts.
Impact Assessment
(inferred). The advisory classifies this as improper input validation in SiteIsolation and does not state exploited-in-the-wild status.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/render_frame_host_impl.cc |
modified | |
BindLambdaForTestingcontent/browser/preloading/prerender/prerender_host_registry_unittest.cc |
modified | |
TEST_Fcontent/browser/preloading/prerender/prerender_host_registry_unittest.cc |
modified |
Files Changed
content/browser/renderer_host/render_frame_host_impl.hcontent/browser/preloading/prerender/prerender_host_registry_unittest.cccontent/common/navigation_client.mojomcontent/browser/renderer_host/render_frame_host_impl.cccontent/test/navigation_simulator_impl.hcontent/renderer/render_frame_impl.cccontent/test/navigation_simulator_impl.cc
Audit Directions
- Renderer-supplied security propertiesAudit other
DidCommitProvisionalLoadParams/ commit-IPC fields andFrameReplicationStatemembers that encode security decisions (trustworthiness, sandbox flags, origin properties) to confirm the browser recomputes rather than trusts them. - Opaque-origin trust checksLook for other places that decide secure-context or powerful-feature eligibility for opaque origins without consulting the precursor tuple via
GetTupleOrPrecursorTupleIfOpaqueandnetwork::IsUrlPotentiallyTrustworthy. - "Clamp instead of recompute" patternsSearch for browser code that merely validates or clamps a renderer-provided boolean instead of deriving it from browser-authoritative state, a recurring class of SiteIsolation input-validation flaws.
Patch
From cbef3d0f82fdb884a10fe58f5cb75fcc3eef50fd Mon Sep 17 00:00:00 2001 From: Rakina Zata Amni <[email protected]> Date: Sun, 02 Aug 2026 20:56:10 -0700 Subject: [PATCH] Remove DidCommitParams has_potentially_trustworthy_unique_origin Instead of passing has_potentially_trustworthy_unique_origin from the renderer and clamping it in the browser, this CL removes the field from DidCommitProvisionalLoadParams entirely and computes it in the browser process inside RenderFrameHostImpl::SetLastCommittedOrigin. This is a security improvement as it prevents a compromised renderer from claiming that an opaque origin is potentially trustworthy. Bug: 496595299 TAG=agy CONV=16ca3df5-92fb-4b33-a9bf-29956e520fc5 Change-Id: I032fc6c526130cb431d9772693c398795142b6c9 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8181587 Reviewed-by: Alex Moshchuk <[email protected]> Reviewed-by: Sam McNally <[email protected]> Commit-Queue: Rakina Zata Amni <[email protected]> Cr-Commit-Position: refs/heads/main@{#1672472} --- diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc index 59c3e37..48e7b97f 100644 --- a/content/browser/renderer_host/render_frame_host_impl.cc +++ b/content/browser/renderer_host/render_frame_host_impl.cc @@ -5301,8 +5301,7 @@ // The origin is only updated for cross-document navigations. if (!was_within_same_document || !features::IsEnforceSameDocumentOriginInvariantsEnabled()) { - SetLastCommittedOrigin(params.origin, - params.has_potentially_trustworthy_unique_origin); + SetLastCommittedOrigin(params.origin); } // If the navigation was a cross-document navigation and it's not the @@ -5441,14 +5440,13 @@ } } -void RenderFrameHostImpl::SetLastCommittedOrigin( - const url::Origin& origin, - bool is_potentially_trustworthy_unique_origin) { +void RenderFrameHostImpl::SetLastCommittedOrigin(const url::Origin& origin) { last_committed_origin_ = origin; - // TODO(https://crbug.com/40159049): Instead of passing - // `is_potentially_trustworthy_unique_origin`, maybe we can just check if the - // origin is opaque and use ``network::IsOriginPotentiallyTrustworthy()` on - // its precursor origin. + const url::SchemeHostPort& precursor = + origin.GetTupleOrPrecursorTupleIfOpaque(); + bool is_potentially_trustworthy_unique_origin = + origin.opaque() && precursor.IsValid() && + network::IsUrlPotentiallyTrustworthy(precursor.GetURL()); browsing_context_state()->SetCurrentOrigin( origin, is_potentially_trustworthy_unique_origin); } @@ -5459,13 +5457,7 @@ void RenderFrameHostImpl::SetLastCommittedOriginForTesting( const url::Origin& origin) { - // Default setting `is_potentially_trustworthy_unique_origin` to just whether - // the origin is opaque or not, since we don't really have a way to get the - // correct value from a random origin. Since this function is used mostly for - // unit tests that won't actually use this value (which is only used in the - // renderer), it should be good enough. - SetLastCommittedOrigin( - origin, /*is_potentially_trustworthy_unique_origin=*/origin.opaque()); + SetLastCommittedOrigin(origin); } const url::Origin& RenderFrameHostImpl::ComputeTopFrameOrigin( @@ -5824,19 +5816,7 @@ GetStoragePartition()->IncrementActiveDocumentCount( GetNetworkIsolationKey()); } - // The `is_potentially_trustworthy_unique_origin` bit should be inherited from - // the creator frame if it exists. Note that we do this even when the new - // frame is sandboxed, following `DocumentLoader::CaclculateOrigin()`. - // TODO(https://crbug.com/40159049): Once we can always trust - // `network::IsOriginPotentiallyTrustworthy()` instead of passing around - // `has_potentially_trustworthy_unique_origin`, remove this. - bool is_potentially_trustworthy_unique_origin = - creator_frame ? creator_frame->browsing_context_state() - ->current_replication_state() - .has_potentially_trustworthy_unique_origin - : false; - SetLastCommittedOrigin(new_frame_origin, - is_potentially_trustworthy_unique_origin); + SetLastCommittedOrigin(new_frame_origin); if (!creator_frame || !creator_frame->is_error_document_) { frame_tree_node()->set_last_successful_origin(new_frame_origin); } diff --git a/content/browser/renderer_host/render_frame_host_impl.h b/content/browser/renderer_host/render_frame_host_impl.h index aa51bc7..2b56bf69 100644 --- a/content/browser/renderer_host/render_frame_host_impl.h +++ b/content/browser/renderer_host/render_frame_host_impl.h @@ -4010,8 +4010,7 @@ // Update this frame's last committed origin. This will also update the origin // and the "has_potentially_trustworthy_unique_origin" bit in the // FrameReplicationState. - void SetLastCommittedOrigin(const url::Origin& origin, - bool is_potentially_trustworthy_unique_origin); + void SetLastCommittedOrigin(const url::Origin& origin); // Stores a snapshot of the inherited base URL from the initiator's // FrameLoadRequest, if this document inherited one (e.g., about:srcdoc). diff --git a/content/common/navigation_client.mojom b/content/common/navigation_client.mojom index e038cee..f6becf5 100644 --- a/content/common/navigation_client.mojom +++ b/content/common/navigation_client.mojom @@ -159,10 +159,6 @@ // enforcing. array<uint32> insecure_navigations_set; - // True if the document for the load is a unique origin that should be - // considered potentially trustworthy. - bool has_potentially_trustworthy_unique_origin; - // Request ID generated by the renderer. int32 request_id; diff --git a/content/renderer/render_frame_impl.cc b/content/renderer/render_frame_impl.cc index 746d0107..2dda49f 100644 --- a/content/renderer/render_frame_impl.cc +++ b/content/renderer/render_frame_impl.cc @@ -5178,9 +5178,6 @@ params->insecure_request_policy = frame_->GetInsecureRequestPolicy(); params->insecure_navigations_set = frame_->GetInsecureRequestToUpgrade(); - params->has_potentially_trustworthy_unique_origin = - frame_origin.IsOpaque() && frame_origin.IsPotentiallyTrustworthy(); - // Set the URL to be displayed in the browser UI to the user. Note this might // be different than the URL actually used in the DocumentLoader (see comments // in GetLoadingUrl() and MaybeGetOverriddenURL()). This might not be the URL diff --git a/content/browser/preloading/prerender/prerender_host_registry_unittest.cc b/content/browser/preloading/prerender/prerender_host_registry_unittest.cc index cdaf2fcd..cd144968 100644 --- a/content/browser/preloading/prerender/prerender_host_registry_unittest.cc +++ b/content/browser/preloading/prerender/prerender_host_registry_unittest.cc @@ -1796,19 +1796,6 @@ insecure_navigations); } -TEST_F(PrerenderHostRegistryTest, - HasPotentiallyTrustworthyUniqueOriginIsSetWhilePrerendering) { - SetupPrerenderAndCommit( - base::BindLambdaForTesting([](NavigationSimulatorImpl* navigation) { - navigation->set_has_potentially_trustworthy_unique_origin(true); - })); - EXPECT_TRUE(contents() - ->GetPrimaryMainFrame() - ->frame_tree_node() - ->current_replication_state() - .has_potentially_trustworthy_unique_origin); -} - // End replication state matching tests ------------ TEST_F(PrerenderHostRegistryTest, OneTaskToDeleteAllHosts) { diff --git a/content/test/navigation_simulator_impl.cc b/content/test/navigation_simulator_impl.cc index d3200ab..fa8b6b1 100644 --- a/content/test/navigation_simulator_impl.cc +++ b/content/test/navigation_simulator_impl.cc @@ -1723,8 +1723,6 @@ params->insecure_request_policy = insecure_request_policy_; params->insecure_navigations_set = insecure_navigations_set_; - params->has_potentially_trustworthy_unique_origin = - has_potentially_trustworthy_unique_origin_; params->commit_navigation_start = base::TimeTicks::Now(); params->commit_navigation_end = base::TimeTicks::Now(); diff --git a/content/test/navigation_simulator_impl.h b/content/test/navigation_simulator_impl.h index 7bf8fc1..90c8ee2 100644 --- a/content/test/navigation_simulator_impl.h +++ b/content/test/navigation_simulator_impl.h @@ -217,12 +217,6 @@ insecure_navigations_set_ = insecure_navigations_set; } - void set_has_potentially_trustworthy_unique_origin( - bool has_potentially_trustworthy_unique_origin) { - has_potentially_trustworthy_unique_origin_ = - has_potentially_trustworthy_unique_origin; - } - void set_supports_loading_mode_header(std::string value) { supports_loading_mode_header_ = value; } @@ -405,7 +399,6 @@ blink::mojom::InsecureRequestPolicy insecure_request_policy_ = blink::mojom::InsecureRequestPolicy::kLeaveInsecureRequestsAlone; std::vector<uint32_t> insecure_navigations_set_; - bool has_potentially_trustworthy_unique_origin_ = false; // Any DNS aliases, as read from CNAME records, for the request URL that // would be in the network::mojom::URLResponseHead. The alias chain order
Regression Test / PoC
diff --git a/content/browser/preloading/prerender/prerender_host_registry_unittest.cc b/content/browser/preloading/prerender/prerender_host_registry_unittest.cc
index cdaf2fcd..cd144968 100644
--- a/content/browser/preloading/prerender/prerender_host_registry_unittest.cc
+++ b/content/browser/preloading/prerender/prerender_host_registry_unittest.cc
@@ -1796,19 +1796,6 @@
insecure_navigations);
}
-TEST_F(PrerenderHostRegistryTest,
- HasPotentiallyTrustworthyUniqueOriginIsSetWhilePrerendering) {
- SetupPrerenderAndCommit(
- base::BindLambdaForTesting([](NavigationSimulatorImpl* navigation) {
- navigation->set_has_potentially_trustworthy_unique_origin(true);
- }));
- EXPECT_TRUE(contents()
- ->GetPrimaryMainFrame()
- ->frame_tree_node()
- ->current_replication_state()
- .has_potentially_trustworthy_unique_origin);
-}
-
// End replication state matching tests ------------
TEST_F(PrerenderHostRegistryTest, OneTaskToDeleteAllHosts) {
diff --git a/content/test/navigation_simulator_impl.cc b/content/test/navigation_simulator_impl.cc
index d3200ab..fa8b6b1 100644
--- a/content/test/navigation_simulator_impl.cc
+++ b/content/test/navigation_simulator_impl.cc
@@ -1723,8 +1723,6 @@
params->insecure_request_policy = insecure_request_policy_;
params->insecure_navigations_set = insecure_navigations_set_;
- params->has_potentially_trustworthy_unique_origin =
- has_potentially_trustworthy_unique_origin_;
params->commit_navigation_start = base::TimeTicks::Now();
params->commit_navigation_end = base::TimeTicks::Now();
diff --git a/content/test/navigation_simulator_impl.h b/content/test/navigation_simulator_impl.h
index 7bf8fc1..90c8ee2 100644
--- a/content/test/navigation_simulator_impl.h
+++ b/content/test/navigation_simulator_impl.h
@@ -217,12 +217,6 @@
insecure_navigations_set_ = insecure_navigations_set;
}
- void set_has_potentially_trustworthy_unique_origin(
- bool has_potentially_trustworthy_unique_origin) {
- has_potentially_trustworthy_unique_origin_ =
- has_potentially_trustworthy_unique_origin;
- }
-
void set_supports_loading_mode_header(std::string value) {
supports_loading_mode_header_ = value;
}
@@ -405,7 +399,6 @@
blink::mojom::InsecureRequestPolicy insecure_request_policy_ =
blink::mojom::InsecureRequestPolicy::kLeaveInsecureRequestsAlone;
std::vector<uint32_t> insecure_navigations_set_;
- bool has_potentially_trustworthy_unique_origin_ = false;
// Any DNS aliases, as read from CNAME records, for the request URL that
// would be in the network::mojom::URLResponseHead. The alias chain order
Original Bug Report
Potential: Compromised renderer can bypass Secure Context ancestor checks via spoofed replication state
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A compromised renderer can spoof the has_potentially_trustworthy_unique_origin flag during a navigation commit. The browser process blindly trusts this flag, stores it, and broadcasts it to sibling renderers. This allows a cross-process child frame to erroneously treat an insecure ancestor as a Secure Context, bypassing W3C requirements for restricted APIs like Service Workers.
Affected files:
content/browser/renderer_host/render_frame_host_impl.cccontent/browser/renderer_host/browsing_context_state.ccthird_party/blink/renderer/core/frame/local_dom_window.ccthird_party/blink/renderer/core/frame/remote_frame.ccthird_party/blink/renderer/platform/weborigin/security_origin.cc
Estimated timestamp from git blame: 2025-07-10
Summary
A compromised renderer process can send a spoofed has_potentially_trustworthy_unique_origin boolean in the DidCommitProvisionalLoadParams IPC during a navigation commit. The browser process accepts this value without validation and broadcasts it to other renderer processes.
This flaw allows a cross-process descendant frame, which should be considered an insecure context due to an insecure ancestor, to incorrectly obtain isSecureContext=true. This erroneously grants the child frame access to sensitive APIs (e.g., Service Workers, WebAuthn) that are gated behind W3C Secure Contexts requirements (§3.1), completely bypassing the ancestor-chain security check.
Vulnerability Details
- Renderer Spoofing: When a navigation commits, the renderer sends a
DidCommitProvisionalLoadParamsIPC containing thehas_potentially_trustworthy_unique_originflag. A compromised renderer can maliciously set this totruefor an opaque origin (e.g., a sandboxed<iframe>). - Lack of Browser Validation: In
content/browser/renderer_host/render_frame_host_impl.cc, withinRenderFrameHostImpl::DidNavigate, the browser process extracts this flag directly from the IPC parameters and passes it toSetLastCommittedOrigin. Crucially,RenderFrameHostImpl::ValidateDidCommitParamsdoes not validate this specific flag.A TODO comment in// render_frame_host_impl.cc:5220 SetLastCommittedOrigin(params.origin, params.has_potentially_trustworthy_unique_origin);SetLastCommittedOriginexplicitly acknowledges that the browser should calculate this value itself usingnetwork::IsOriginPotentiallyTrustworthy()instead of trusting the renderer. - State Replication:
SetLastCommittedOrigincallsBrowsingContextState::SetCurrentOrigin, which stores the poisoned flag in the browser’sreplication_state_and immediately broadcasts it to allRenderFrameProxyHosts in other renderer processes. - Poisoning the RemoteFrame: In a clean renderer process (e.g., one hosting a cross-origin child frame),
RemoteFrame::SetReplicatedOriginreceives the broadcast. It copies the origin and applies the poisoned flag by callingsecurity_origin->SetOpaqueOriginIsPotentiallyTrustworthy(true). - Bypassing the Ancestor Check: When the clean child frame evaluates its secure context status,
LocalDOMWindow::HasInsecureContextInAncestorswalks up the frame tree. For the spoofed ancestor’sRemoteFrame, it callsSecurityOrigin::IsPotentiallyTrustworthy(). Because the origin is opaque, this method returns the poisonedis_opaque_origin_potentially_trustworthy_flag (true). - Erroneous API Access: The descendant frame’s
secure_context_mode_is set tokSecureContext. The clean renderer now erroneously exposes Secure-Context-gated APIs (like Service Workers) to the child frame, despite it having an insecure ancestor.
Furthermore, browser-side checks for API access often fail to re-verify the ancestor chain. For example, ServiceWorkerContainerHostForClient::Register only checks if the frame’s own origin is potentially trustworthy, relying on the renderer’s isSecureContext evaluation for the ancestor chain requirement.
Potential Reproduction Steps
(Note: These are suggested steps; a working PoC has not been executed yet.)
- Host a top-level page (
https://top.example) that embeds an attacker-controlled, sandboxed iframe:<iframe sandbox='allow-scripts' src='http://attacker.example/mid.html'>. mid.htmlembeds a cross-origin victim iframe:<iframe src='https://victim.example/child.html'>. Due to Site Isolation,child.htmlis in a different process.- Exploit a vulnerability in the renderer hosting
mid.htmlto achieve RCE. - During the navigation commit for
mid.html, use the RCE to modify theDidCommitProvisionalLoadParamsIPC, settinghas_potentially_trustworthy_unique_origin = true. - The browser receives the IPC, updates its state, and broadcasts the poisoned flag to the clean renderer process hosting
child.html. - When
child.htmlcommits, itsLocalDOMWindow::HasInsecureContextInAncestorscheck will evaluate the spoofedRemoteFrameas trustworthy. window.isSecureContextwill betrueinchild.html. Ifchild.htmlattempts to register a Service Worker (e.g., via an XSS or its own legitimate code), the registration will succeed, compromising the victim’s origin.
Suggested Fix
Remove the has_potentially_trustworthy_unique_origin field from mojom::DidCommitProvisionalLoadParams. The browser process should exclusively determine the trustworthiness of an origin during navigation commits.
As noted in the existing TODO in RenderFrameHostImpl::SetLastCommittedOrigin (crbug.com/1153336 or crbug.com/40159049), the browser should check if the origin is opaque and use network::IsOriginPotentiallyTrustworthy() on its precursor origin, rather than trusting the potentially compromised renderer process.
Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. Please feel free to reach out to me if you have concerns or feedback.