Chrome · Loader
CVE-2026-87575
Logic Error in Loader
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/html/anchor_element_utils.cc |
modified | |
UserGestureNavigationTestWebFrameClientthird_party/blink/renderer/core/loader/frame_loader_test.cc |
modified |
Files Changed
third_party/blink/renderer/core/frame/local_dom_window.ccthird_party/blink/renderer/core/frame/location.ccthird_party/blink/renderer/core/html/anchor_element_utils.ccthird_party/blink/renderer/core/html/html_anchor_element.ccthird_party/blink/renderer/core/html/html_frame_owner_element.ccthird_party/blink/renderer/core/loader/form_submission.ccthird_party/blink/renderer/core/loader/frame_load_request.ccthird_party/blink/renderer/core/loader/frame_load_request.hthird_party/blink/renderer/core/loader/frame_loader.ccthird_party/blink/renderer/core/loader/frame_loader_test.cc
Patch
From f44ff6bf5ee043562369b9f184234010246ed09e Mon Sep 17 00:00:00 2001 From: Takashi Toyoshima <[email protected]> Date: Mon, 17 Aug 2026 11:52:49 -0700 Subject: [PATCH] Fix user gesture misattribution in same-process navigation FrameLoader::StartNavigation unconditionally set the target LocalFrame's transient user activation on ResourceRequest::has_user_gesture_. When a same-process cross-origin initiator navigated a target frame that had transient user activation, the resulting navigation request incorrectly inherited the target frame's user gesture. This resulted in Sec-Fetch-User: ?1 being incorrectly attached and bypassed user-gesture gates in the browser process. This CL: 1. Removes the target frame activation overwrite in FrameLoader. 2. Updates FrameLoadRequest's constructor to automatically capture the initiator frame's transient user activation from origin_window (aligning with RequestorOrigin, InitiatorStateToken, and Referrer), preventing user gesture dropping across all current and future entry points by design. 3. Cleans up redundant SetHasUserGesture() calls at individual call sites now that FrameLoadRequest handles it automatically, and ensures AnchorElementUtils::HandleDownloadAttribute sets user gesture for <a download> downloads. TAG=agy CONV=96f28c27-e299-44c6-b29d-a2883ed49956 Bug: 540013886 Change-Id: I3b17b79ce66a99173ec22ad8aeab693b679bdbf8 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8254506 Commit-Queue: Takashi Toyoshima <[email protected]> Reviewed-by: Nate Chapin <[email protected]> Cr-Commit-Position: refs/heads/main@{#1680668} --- diff --git a/third_party/blink/renderer/core/frame/local_dom_window.cc b/third_party/blink/renderer/core/frame/local_dom_window.cc index e60957d..74cc1c0b 100644 --- a/third_party/blink/renderer/core/frame/local_dom_window.cc +++ b/third_party/blink/renderer/core/frame/local_dom_window.cc @@ -2533,9 +2533,6 @@ frame_request.GetResourceRequest().SetReferrerPolicy( referrer.referrer_policy); - bool has_user_gesture = LocalFrame::HasTransientUserActivation(GetFrame()); - frame_request.GetResourceRequest().SetHasUserGesture(has_user_gesture); - FrameTree::FindResult result = GetFrame()->Tree().FindOrCreateFrameForNavigation( frame_request, target.empty() ? AtomicString("_blank") : target); diff --git a/third_party/blink/renderer/core/frame/location.cc b/third_party/blink/renderer/core/frame/location.cc index 81b9b79..69e701e 100644 --- a/third_party/blink/renderer/core/frame/location.cc +++ b/third_party/blink/renderer/core/frame/location.cc @@ -333,9 +333,6 @@ } ResourceRequestHead resource_request(completed_url); - resource_request.SetHasUserGesture( - LocalFrame::HasTransientUserActivation(incumbent_window->GetFrame())); - FrameLoadRequest request(incumbent_window, resource_request); request.SetClientNavigationReason(ClientNavigationReason::kFrameNavigation); WebFrameLoadType frame_load_type = WebFrameLoadType::kStandard; diff --git a/third_party/blink/renderer/core/html/anchor_element_utils.cc b/third_party/blink/renderer/core/html/anchor_element_utils.cc index 2faa155f..5e2c18e8 100644 --- a/third_party/blink/renderer/core/html/anchor_element_utils.cc +++ b/third_party/blink/renderer/core/html/anchor_element_utils.cc @@ -151,6 +151,7 @@ request.SetSuggestedFilename(download_attr); request.SetRequestContext(mojom::blink::RequestContextType::DOWNLOAD); request.SetRequestorOrigin(window->GetSecurityOrigin()); + request.SetHasUserGesture(LocalFrame::HasTransientUserActivation(frame)); network::mojom::ReferrerPolicy referrer_policy = request.GetReferrerPolicy(); if (referrer_policy == network::mojom::ReferrerPolicy::kDefault) { diff --git a/third_party/blink/renderer/core/html/html_anchor_element.cc b/third_party/blink/renderer/core/html/html_anchor_element.cc index 512d24e..f647249f 100644 --- a/third_party/blink/renderer/core/html/html_anchor_element.cc +++ b/third_party/blink/renderer/core/html/html_anchor_element.cc @@ -494,8 +494,6 @@ link_relations_, GetDocument()); LocalFrame* frame = window->GetFrame(); - request.SetHasUserGesture(LocalFrame::HasTransientUserActivation(frame)); - NavigationPolicy navigation_policy = NavigationPolicyFromEvent(&event); // Respect the download attribute only if we can read the content, and the diff --git a/third_party/blink/renderer/core/html/html_frame_owner_element.cc b/third_party/blink/renderer/core/html/html_frame_owner_element.cc index eb18037..934a600 100644 --- a/third_party/blink/renderer/core/html/html_frame_owner_element.cc +++ b/third_party/blink/renderer/core/html/html_frame_owner_element.cc @@ -772,8 +772,6 @@ KURL url_to_request = url.IsNull() ? BlankUrl() : url; ResourceRequestHead request(url_to_request); request.SetReferrerPolicy(ReferrerPolicyAttribute()); - request.SetHasUserGesture( - LocalFrame::HasTransientUserActivation(GetDocument().GetFrame())); network::mojom::blink::TrustTokenParamsPtr trust_token_params = ConstructTrustTokenParams(); diff --git a/third_party/blink/renderer/core/loader/form_submission.cc b/third_party/blink/renderer/core/loader/form_submission.cc index 2a0243f5..dec95d4f 100644 --- a/third_party/blink/renderer/core/loader/form_submission.cc +++ b/third_party/blink/renderer/core/loader/form_submission.cc @@ -349,8 +349,6 @@ } } LocalFrame* form_local_frame = form->GetDocument().GetFrame(); - resource_request->SetHasUserGesture( - LocalFrame::HasTransientUserActivation(form_local_frame)); resource_request->SetFormSubmission(true); mojom::blink::TriggeringEventInfo triggering_event_info; diff --git a/third_party/blink/renderer/core/loader/frame_load_request.cc b/third_party/blink/renderer/core/loader/frame_load_request.cc index b10e999d..5bc9cd2f 100644 --- a/third_party/blink/renderer/core/loader/frame_load_request.cc +++ b/third_party/blink/renderer/core/loader/frame_load_request.cc @@ -13,6 +13,7 @@ #include "third_party/blink/renderer/core/events/current_input_event.h" #include "third_party/blink/renderer/core/fileapi/public_url_manager.h" #include "third_party/blink/renderer/core/frame/local_dom_window.h" +#include "third_party/blink/renderer/core/frame/local_frame.h" #include "third_party/blink/renderer/core/frame/policy_container.h" #include "third_party/blink/renderer/core/html/forms/html_form_element.h" #include "third_party/blink/renderer/core/script_tools/script_tool_context.h" @@ -116,6 +117,12 @@ SetReferrerForRequest(origin_window, resource_request_); + if (origin_window->GetFrame()) { + resource_request_.SetHasUserGesture( + resource_request_.HasUserGesture() || + LocalFrame::HasTransientUserActivation(origin_window->GetFrame())); + } + SetSourceLocation(CaptureSourceLocation(origin_window)); // If a Script Tool (WebMCP tool) execution is currently active in the diff --git a/third_party/blink/renderer/core/loader/frame_load_request.h b/third_party/blink/renderer/core/loader/frame_load_request.h index 1f9520bd..1b4367c 100644 --- a/third_party/blink/renderer/core/loader/frame_load_request.h +++ b/third_party/blink/renderer/core/loader/frame_load_request.h @@ -60,6 +60,9 @@ STACK_ALLOCATED(); public: + // Automatically populates resource_request.has_user_gesture from + // `origin_window`'s transient user activation state when `origin_window` is + // non-null (defaults to false otherwise, unless already set). FrameLoadRequest(LocalDOMWindow* origin_window, const ResourceRequest&); FrameLoadRequest(LocalDOMWindow* origin_window, const ResourceRequestHead&); FrameLoadRequest(const FrameLoadRequest&) = delete; diff --git a/third_party/blink/renderer/core/loader/frame_loader.cc b/third_party/blink/renderer/core/loader/frame_loader.cc index 08bd9c2..cd5f06b 100644 --- a/third_party/blink/renderer/core/loader/frame_loader.cc +++ b/third_party/blink/renderer/core/loader/frame_loader.cc @@ -715,9 +715,6 @@ url.GetString().Utf8(), "load_type", static_cast<int>(frame_load_type)); - resource_request.SetHasUserGesture( - LocalFrame::HasTransientUserActivation(frame_.Get())); - if (!AllowRequestForThisFrame(request)) return; diff --git a/third_party/blink/renderer/core/loader/frame_loader_test.cc b/third_party/blink/renderer/core/loader/frame_loader_test.cc index fd9c455..7a915a29 100644 --- a/third_party/blink/renderer/core/loader/frame_loader_test.cc +++ b/third_party/blink/renderer/core/loader/frame_loader_test.cc @@ -18,6 +18,7 @@ #include "third_party/blink/renderer/core/frame/local_frame.h" #include "third_party/blink/renderer/core/frame/policy_container.h" #include "third_party/blink/renderer/core/html/html_anchor_element.h" +#include "third_party/blink/renderer/core/loader/frame_load_request.h" #include "third_party/blink/renderer/core/page/chrome_client_impl.h" #include "third_party/blink/renderer/core/testing/mock_policy_container_host.h" #include "third_party/blink/renderer/core/testing/sim/sim_request.h" @@ -325,4 +326,87 @@ SchemeRegistry::RemoveURLSchemeAsDirectLaunchForTest(kScheme); } +class UserGestureNavigationTestWebFrameClient + : public frame_test_helpers::TestWebFrameClient { + public: + void BeginNavigation(std::unique_ptr<WebNavigationInfo> info) override { + last_has_user_gesture_ = info->url_request.HasUserGesture(); + } + + std::optional<bool> last_has_user_gesture() const { + return last_has_user_gesture_; + } + + private: + std::optional<bool> last_has_user_gesture_; +}; +
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/third_party/blink/renderer/core/loader/frame_loader_test.cc b/third_party/blink/renderer/core/loader/frame_loader_test.cc
index fd9c455..7a915a29 100644
--- a/third_party/blink/renderer/core/loader/frame_loader_test.cc
+++ b/third_party/blink/renderer/core/loader/frame_loader_test.cc
@@ -18,6 +18,7 @@
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/policy_container.h"
#include "third_party/blink/renderer/core/html/html_anchor_element.h"
+#include "third_party/blink/renderer/core/loader/frame_load_request.h"
#include "third_party/blink/renderer/core/page/chrome_client_impl.h"
#include "third_party/blink/renderer/core/testing/mock_policy_container_host.h"
#include "third_party/blink/renderer/core/testing/sim/sim_request.h"
@@ -325,4 +326,87 @@
SchemeRegistry::RemoveURLSchemeAsDirectLaunchForTest(kScheme);
}
+class UserGestureNavigationTestWebFrameClient
+ : public frame_test_helpers::TestWebFrameClient {
+ public:
+ void BeginNavigation(std::unique_ptr<WebNavigationInfo> info) override {
+ last_has_user_gesture_ = info->url_request.HasUserGesture();
+ }
+
+ std::optional<bool> last_has_user_gesture() const {
+ return last_has_user_gesture_;
+ }
+
+ private:
+ std::optional<bool> last_has_user_gesture_;
+};
+
+TEST_F(FrameLoaderTest, StartNavigationDoesNotSpoofTargetUserGesture) {
+ UserGestureNavigationTestWebFrameClient client;
+ frame_test_helpers::WebViewHelper helper;
+ helper.Initialize(&client);
+
+ LocalFrame* target_frame = helper.LocalMainFrame()->GetFrame();
+
+ // Target frame has transient user activation.
+ LocalFrame::NotifyUserActivation(
+ target_frame, mojom::UserActivationNotificationType::kTest);
+ ASSERT_TRUE(LocalFrame::HasTransientUserActivation(target_frame));
+
+ // Initiator frame load request without user activation.
+ ResourceRequest resource_request(KURL("https://example.com/foo.html"));
+ resource_request.SetHasUserGesture(false);
+ FrameLoadRequest request(nullptr, resource_request);
+
+ target_frame->Loader().StartNavigation(request);
+
+ ASSERT_TRUE(client.last_has_user_gesture().has_value());
+ EXPECT_FALSE(client.last_has_user_gesture().value());
+}
+
+TEST_F(FrameLoaderTest, StartNavigationPropagatesInitiatorUserGesture) {
+ UserGestureNavigationTestWebFrameClient client;
+ frame_test_helpers::WebViewHelper helper;
+ helper.Initialize(&client);
+
+ LocalFrame* target_frame = helper.LocalMainFrame()->GetFrame();
+
+ // Target frame has NO transient user activation.
+ ASSERT_FALSE(LocalFrame::HasTransientUserActivation(target_frame));
+
+ // Initiator frame load request with user activation.
+ ResourceRequest resource_request(KURL("https://example.com/foo.html"));
+ resource_request.SetHasUserGesture(true);
+ FrameLoadRequest request(nullptr, resource_request);
+
+ target_frame->Loader().StartNavigation(request);
+
+ ASSERT_TRUE(client.last_has_user_gesture().has_value());
+ EXPECT_TRUE(client.last_has_user_gesture().value());
+}
+
+TEST_F(FrameLoaderTest, FrameLoadRequestCapturesInitiatorUserGesture) {
+ UserGestureNavigationTestWebFrameClient client;
+ frame_test_helpers::WebViewHelper helper;
+ helper.Initialize(&client);
+
+ LocalFrame* initiator_frame = helper.LocalMainFrame()->GetFrame();
+
+ // 1. Without user activation on initiator.
+ ASSERT_FALSE(LocalFrame::HasTransientUserActivation(initiator_frame));
+ FrameLoadRequest request1(
+ initiator_frame->DomWindow(),
+ ResourceRequest(KURL("https://example.com/foo.html")));
+ EXPECT_FALSE(request1.GetResourceRequest().HasUserGesture());
+
+ // 2. With user activation on initiator.
+ LocalFrame::NotifyUserActivation(
+ initiator_frame, mojom::UserActivationNotificationType::kTest);
+ ASSERT_TRUE(LocalFrame::HasTransientUserActivation(initiator_frame));
+ FrameLoadRequest request2(
+ initiator_frame->DomWindow(),
+ ResourceRequest(KURL("https://example.com/foo.html")));
+ EXPECT_TRUE(request2.GetResourceRequest().HasUserGesture());
+}
+
} // namespace blink
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page