Low chrome Logic Error 🔧 Commit mapped

Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Loader
DescriptionIncorrect authorization in Loader
ComponentLoader
Bug ClassLogic Error
Tracker540013886
Fix commitf44ff6bf5ee0 (chromium/src) +95/-18
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/html/anchor_element_utils.cc
modified
UserGestureNavigationTestWebFrameClient
third_party/blink/renderer/core/loader/frame_loader_test.cc
modified

Files Changed

  • third_party/blink/renderer/core/frame/local_dom_window.cc
  • third_party/blink/renderer/core/frame/location.cc
  • third_party/blink/renderer/core/html/anchor_element_utils.cc
  • third_party/blink/renderer/core/html/html_anchor_element.cc
  • third_party/blink/renderer/core/html/html_frame_owner_element.cc
  • third_party/blink/renderer/core/loader/form_submission.cc
  • third_party/blink/renderer/core/loader/frame_load_request.cc
  • third_party/blink/renderer/core/loader/frame_load_request.h
  • third_party/blink/renderer/core/loader/frame_loader.cc
  • third_party/blink/renderer/core/loader/frame_loader_test.cc
From f44ff6bf5ee043562369b9f184234010246ed09e Mon Sep 17 00:00:00 2001
From: Takashi Toyoshima <toyoshim@chromium.org>
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 <toyoshim@chromium.org>
Reviewed-by: Nate Chapin <japhet@chromium.org>
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.