Low watch CVSS 5.4 chrome Authorization Error 🔧 Commit mapped

Overview

Low
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect authorization in Loader
DescriptionIncorrect authorization in Loader
ComponentLoader
Bug ClassAuthorization Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker540013886
Fix commitf44ff6bf5ee0 (chromium/src) +95/-18
CWECWE-863
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-07-28 (per vendor advisory)
Fix landed2026-08-17 (public git)
Disclosed2026-09-08
Patch gap22 days — fix public in source before the advisory

Background

Transient user activation
a short-lived per-frame flag, set by a real user interaction (click, keypress) and queried via LocalFrame::HasTransientUserActivation, that gates privileged actions such as popups, downloads, and fullscreen.
`has_user_gesture_` (`ResourceRequest`)
a per-request boolean, set with SetHasUserGesture and read with HasUserGesture, that records whether the navigation request was driven by a user gesture and drives the Sec-Fetch-User: ?1 request header and browser-side user-activation gates.
`FrameLoadRequest`
the Blink object bundling a navigation’s parameters (its origin_window/initiator, requestor origin, referrer, and ResourceRequest) that is passed into FrameLoader::StartNavigation.
Initiator vs. target frame
the initiator is the origin_window’s frame that requested the navigation, while the target is the frame that will actually load the URL; for security the gesture must belong to the initiator, not the target.

Root Cause Analysis

FrameLoader::StartNavigation in frame_loader.cc unconditionally overwrote the request’s gesture flag with resource_request.SetHasUserGesture(LocalFrame::HasTransientUserActivation(frame_.Get())), where frame_.Get() is the target frame being navigated rather than the initiator. When a same-process cross-origin initiator that had no user activation navigated a target frame that did hold transient user activation, the request incorrectly inherited the target’s gesture, so Sec-Fetch-User: ?1 was attached and browser-process user-gesture gates were satisfied by a gesture that never belonged to the initiator. The invariant violated is that a navigation’s user-gesture attribution must reflect the initiating context’s activation, not any frame that happens to hold activation.

The fix removes the target-frame overwrite in StartNavigation and instead has the FrameLoadRequest constructor OR the request’s existing gesture with LocalFrame::HasTransientUserActivation(origin_window->GetFrame()), binding the gesture to the initiator at request-construction time. Because the capture now lives in the shared constructor (alongside RequestorOrigin, referrer, and source location), every current and future entry point is covered by design, and the removed per-call-site SetHasUserGesture calls become redundant.

Key insight
The single mistake was authorizing a navigation’s user gesture from the target frame’s activation instead of the initiator’s, letting an unactivated cross-origin initiator borrow a victim frame’s gesture; the fix moves gesture capture into FrameLoadRequest’s constructor keyed on origin_window, so attribution follows the initiator and can no longer be spoofed by the navigation target’s state.

Attack Path

  1. Set up a same-process cross-origin pair An attacker page arranges to navigate a target frame that is same-process but cross-origin, where the target frame currently holds transient user activation (e.g. from a prior legitimate interaction) while the attacker’s initiator context has none.
  2. Trigger a gesture-free navigation of the target The initiator programmatically navigates the target (e.g. via window.open/location/frame src), producing a ResourceRequest with has_user_gesture_ = false.
  3. Target activation is misattributed FrameLoader::StartNavigation overwrites the flag with the target frame’s HasTransientUserActivation, flipping has_user_gesture_ to true for a request the initiator never activated.
  4. User-gesture gates are bypassed Sec-Fetch-User: ?1 is attached and browser-process checks that require a genuine user gesture (e.g. popup/download/activation-gated flows) are satisfied, so actions that should be blocked proceed (inferred — the patch demonstrates the misattribution but does not enumerate the specific downstream gate reached).

Impact Assessment

An attacker in the renderer gains incorrect user-gesture attribution on a navigation request, forging Sec-Fetch-User: ?1 and bypassing browser-process gates that are meant to require a real user interaction. Preconditions are a same-process, cross-origin initiator/target relationship in which the target frame holds transient user activation while the initiator does not; the escalation is limited to defeating user-gesture authorization (inferred to enable gesture-gated actions such as popups or downloads), not memory corruption. The advisory classifies this as incorrect authorization in the Loader component and does not report exploitation in the wild.

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
TEST_F
third_party/blink/renderer/core/loader/frame_loader_test.cc
modified

Files Changed

  • third_party/blink/renderer/core/loader/frame_loader_test.cc
  • third_party/blink/renderer/core/html/html_anchor_element.cc
  • third_party/blink/renderer/core/loader/frame_load_request.cc
  • third_party/blink/renderer/core/frame/location.cc
  • third_party/blink/renderer/core/loader/frame_loader.cc
  • third_party/blink/renderer/core/loader/frame_load_request.h
  • third_party/blink/renderer/core/html/anchor_element_utils.cc
  • third_party/blink/renderer/core/svg/svg_a_element.cc
  • third_party/blink/renderer/core/html/html_frame_owner_element.cc
  • third_party/blink/renderer/core/mathml/mathml_anchor_element.cc
  • third_party/blink/renderer/core/loader/form_submission.cc
  • third_party/blink/renderer/core/frame/local_dom_window.cc

Audit Directions

  • Target-vs-initiator confusion
    Audit any code that reads security-relevant state (HasTransientUserActivation, origin, referrer policy) from the frame being acted upon rather than the frame that initiated the action.
  • Centralized-vs-scattered attribution
    Look for security attributes set independently at many call sites; scattered writes drift and one can key off the wrong object, so verify a single authoritative capture point exists (as FrameLoadRequest now provides).
  • Sec-Fetch / user-gesture propagation
    Trace every producer of has_user_gesture_ feeding Sec-Fetch-User and browser-side gesture gates to confirm the value is derived from the initiating context and cannot be inflated by a navigation target or unrelated frame.
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/mathml/mathml_anchor_element.cc b/third_party/blink/renderer/core/mathml/mathml_anchor_element.cc
index 3e3dcac..200ad7ee 100644
--- a/third_party/blink/renderer/core/mathml/mathml_anchor_element.cc
+++ b/third_party/blink/renderer/core/mathml/mathml_anchor_element.cc
@@ -132,7 +132,6 @@
       request, FastGetAttribute(html_names::kReferrerpolicyAttr),
       link_relations_, GetDocument());
 
-  request.SetHasUserGesture(LocalFrame::HasTransientUserActivation(frame));
   NavigationPolicy navigation_policy = NavigationPolicyFromEvent(&event);
 
   if (FastHasAttribute(html_names::kDownloadAttr) &&
diff --git a/third_party/blink/renderer/core/svg/svg_a_element.cc b/third_party/blink/renderer/core/svg/svg_a_element.cc
index f54e5ce..d321cb6 100644
--- a/third_party/blink/renderer/core/svg/svg_a_element.cc
+++ b/third_party/blink/renderer/core/svg/svg_a_element.cc
@@ -163,8 +163,6 @@
           request, FastGetAttribute(svg_names::kReferrerpolicyAttr),
           link_relations_, GetDocument());
 
-      request.SetHasUserGesture(LocalFrame::HasTransientUserActivation(frame));
-
       // Respect the download attribute only if we can read the content, and the
       // event is not an alt-click or similar.
       if (FastHasAttribute(svg_names::kDownloadAttr) &&
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…

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.