Medium chrome Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Extensions
DescriptionInsufficient policy enforcement in Extensions
ComponentExtensions
Bug ClassLogic Error
Tracker511772271
Fix commitf66e7ae1e551 (chromium/src) +110/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
IN_PROC_BROWSER_TEST_F
content/browser/download/download_browsertest.cc
modified
IN_PROC_BROWSER_TEST_F
content/browser/security_exploit_browsertest.cc
modified

Files Changed

  • content/browser/bad_message.h
  • content/browser/download/download_browsertest.cc
  • content/browser/renderer_host/ipc_utils.cc
  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/browser/security_exploit_browsertest.cc
  • tools/metrics/histograms/metadata/stability/enums.xml
From f66e7ae1e551fe9220724dc26794e31a841ee631 Mon Sep 17 00:00:00 2001
From: Zainab Rizvi <[email protected]>
Date: Mon, 10 Aug 2026 17:39:31 -0700
Subject: [PATCH] Validate referrer and user gesture in DownloadURL IPC

Verify in the browser process that DownloadURLParams referrer URLs are
hosted by the renderer process and that user gesture claims match the
frame's actual activation state.

Defense in depth: IsOffstoreInstallAllowed also requires the authoritative download URL to match the allowlist, so a spoofed referrer alone does not bypass the policy.

TAG=agy
CONV=8c8e3606-e3f0-4996-91a9-49faa1986cb5

Bug: 511772271
Change-Id: I631b9f68ba560c9492459bcb093e7a0b02b795f4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8205905
Reviewed-by: Bo Liu <[email protected]>
Reviewed-by: Caitlin Fischer <[email protected]>
Commit-Queue: Zainab Rizvi <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1676852}
---

diff --git a/content/browser/bad_message.h b/content/browser/bad_message.h
index 7830fbb..99953da0 100644
--- a/content/browser/bad_message.h
+++ b/content/browser/bad_message.h
@@ -399,6 +399,7 @@
   RFHI_INVALID_CLIENT_SIDE_REDIRECT_URL = 371,
   RFH_BLOB_URL_STORE_ASSOCIATED_PDF_PROCESS_BLOCKED = 372,
   RFH_BLOB_URL_STORE_RECEIVER_PDF_PROCESS_BLOCKED = 373,
+  RFH_DOWNLOAD_URL_INVALID_REFERRER = 374,
 
   // Please add new elements here. The naming convention is abbreviated class
   // name (e.g. RenderFrameHost becomes RFH) plus a unique description of the
diff --git a/content/browser/download/download_browsertest.cc b/content/browser/download/download_browsertest.cc
index ae876f8..065ae24 100644
--- a/content/browser/download/download_browsertest.cc
+++ b/content/browser/download/download_browsertest.cc
@@ -5542,6 +5542,71 @@
   DownloadManagerForShell(shell())->Shutdown();
 }
 
+// Ensure that a real <a download> click preserves the user gesture claim since
+// it legitimately has transient user activation.
+IN_PROC_BROWSER_TEST_F(DownloadContentTest, DownloadURLWithGenuineClick) {
+  GURL download_url =
+      embedded_test_server()->GetURL("/download/download-test.lib");
+
+  EXPECT_TRUE(
+      NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html")));
+  WebContentsImpl* web_contents =
+      static_cast<WebContentsImpl*>(shell()->web_contents());
+
+  EXPECT_TRUE(ExecJs(web_contents,
+                     "let a = document.createElement('a');"
+                     "a.id = 'downloadlink';"
+                     "a.download = 'download-test.lib';"
+                     "a.href = '" +
+                         download_url.spec() +
+                         "';"
+                         "a.innerText = 'click me';"
+                         "document.body.appendChild(a);"));
+
+  std::unique_ptr<DownloadTestObserver> observer(
+      CreateInProgressWaiter(shell(), 1));
+  EXPECT_TRUE(ExecJs(web_contents, "document.getElementById('downloadlink').click()"));
+
+  observer->WaitForFinished();
+
+  std::vector<raw_ptr<download::DownloadItem, VectorExperimental>> downloads;
+  DownloadManagerForShell(shell())->GetAllDownloads(&downloads);
+  ASSERT_EQ(1u, downloads.size());
+  EXPECT_TRUE(downloads[0]->HasUserGesture());
+}
+
+// Ensure that calling DownloadURL from a frame without transient user
+// activation does not preserve a spoofed has_user_gesture claim.
+IN_PROC_BROWSER_TEST_F(DownloadContentTest, DownloadURLWithoutUserActivation) {
+  GURL download_url =
+      embedded_test_server()->GetURL("/download/download-test.lib");
+
+  EXPECT_TRUE(
+      NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html")));
+  WebContentsImpl* web_contents =
+      static_cast<WebContentsImpl*>(shell()->web_contents());
+  RenderFrameHostImpl* main_frame =
+      web_contents->GetPrimaryFrameTree().root()->current_frame_host();
+
+  EXPECT_FALSE(main_frame->HasTransientUserActivation());
+
+  std::unique_ptr<DownloadTestObserver> observer(
+      CreateInProgressWaiter(shell(), 1));
+
+  auto params = blink::mojom::DownloadURLParams::New();
+  params->url = download_url;
+  params->initiator_origin = main_frame->GetLastCommittedOrigin();
+  params->has_user_gesture = true;
+  main_frame->DownloadURL(std::move(params));
+
+  observer->WaitForFinished();
+
+  std::vector<raw_ptr<download::DownloadItem, VectorExperimental>> downloads;
+  DownloadManagerForShell(shell())->GetAllDownloads(&downloads);
+  ASSERT_EQ(1u, downloads.size());
+  EXPECT_FALSE(downloads[0]->HasUserGesture());
+}
+
 using DownloadRangeTestParams =
     std::tuple<int64_t /*starting byte in range request*/,
                int64_t /*ending byte in range request*/,
diff --git a/content/browser/renderer_host/ipc_utils.cc b/content/browser/renderer_host/ipc_utils.cc
index ee707728..66e72aff 100644
--- a/content/browser/renderer_host/ipc_utils.cc
+++ b/content/browser/renderer_host/ipc_utils.cc
@@ -200,6 +200,17 @@
       !VerifyInitiatorOrigin(process_id, *params.initiator_origin))
     return false;
 
+  // Verify |params.referrer|.
+  if (params.referrer && !params.referrer->url.is_empty()) {
+    auto* policy = ChildProcessSecurityPolicyImpl::GetInstance();
+    if (!policy->HostsOrigin(process_id.GetUnsafeValue(),
+                             url::Origin::Create(params.referrer->url))) {
+      bad_message::ReceivedBadMessage(
+          process_id, bad_message::RFH_DOWNLOAD_URL_INVALID_REFERRER);
+      return false;
+    }
+  }
+
   // If |params.url| is not set, this must be a large data URL being passed
   // through |params.data_url_blob|.
   if (!params.url.is_valid() && !params.data_url_blob.is_valid())
diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 253d0b8..f383f99 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -8000,7 +8000,9 @@
   std::unique_ptr<download::DownloadUrlParameters> parameters =
       CreateDownloadUrlParameters(blink_parameters->url, traffic_annotation);
   parameters->set_content_initiated(!blink_parameters->is_context_menu_save);
-  parameters->set_has_user_gesture(blink_parameters->has_user_gesture);
+  // Ensure that user gesture claims match the current activation state.
+  parameters->set_has_user_gesture(blink_parameters->has_user_gesture &&
+                                   HasTransientUserActivation());
   parameters->set_suggested_name(
       blink_parameters->suggested_name.value_or(std::u16string()));
   parameters->set_prompt(blink_parameters->is_context_menu_save);
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index ade25ac..19bb216 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -5701,4 +5701,33 @@
   EXPECT_FALSE(subframe->IsRenderFrameLive());
 }
 
+// Ensure that the renderer is terminated if it sends a DownloadURL IPC with a
+// referrer URL that the renderer's process is not allowed to host.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, DownloadURLInvalidReferrer) {
+  // Explicitly isolating a.com helps ensure that this test is applicable on
+  // platforms without site-per-process.
+  IsolateOrigin("a.com");
+
+  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+  WebContentsImpl* web_contents =
+      static_cast<WebContentsImpl*>(shell()->web_contents());
+  RenderFrameHostImpl* main_frame =
+      web_contents->GetPrimaryFrameTree().root()->current_frame_host();
+
+  // Simulate that the renderer sends a DownloadURL IPC claiming that the
+  // download was referred by a page on a different site.
+  auto params = blink::mojom::DownloadURLParams::New();
+  params->url = embedded_test_server()->GetURL("a.com", "/title2.html");
+  params->initiator_origin = main_frame->GetLastCommittedOrigin();
+  params->referrer = blink::mojom::Referrer::New();
+  params->referrer->url =
+      embedded_test_server()->GetURL("b.com", "/title1.html");
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess());
+  main_frame->DownloadURL(std::move(params));
+  EXPECT_EQ(bad_message::RFH_DOWNLOAD_URL_INVALID_REFERRER, kill_waiter.Wait());
+  EXPECT_FALSE(main_frame->IsRenderFrameLive());
+}
+
 }  // namespace content
diff --git a/tools/metrics/histograms/metadata/stability/enums.xml b/tools/metrics/histograms/metadata/stability/enums.xml
index 5015d75d..396a174 100644
--- a/tools/metrics/histograms/metadata/stability/enums.xml
+++ b/tools/metrics/histograms/metadata/stability/enums.xml
@@ -550,6 +550,7 @@
   <int value="371" label="RFHI_INVALID_CLIENT_SIDE_REDIRECT_URL"/>
   <int value="372" label="RFH_BLOB_URL_STORE_ASSOCIATED_PDF_PROCESS_BLOCKED"/>
   <int value="373" label="RFH_BLOB_URL_STORE_RECEIVER_PDF_PROCESS_BLOCKED"/>
+  <int value="374" label="RFH_DOWNLOAD_URL_INVALID_REFERRER"/>
 </enum>
 
 <enum name="BadMessageReasonExtensions">
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/download/download_browsertest.cc b/content/browser/download/download_browsertest.cc
index ae876f8..065ae24 100644
--- a/content/browser/download/download_browsertest.cc
+++ b/content/browser/download/download_browsertest.cc
@@ -5542,6 +5542,71 @@
   DownloadManagerForShell(shell())->Shutdown();
 }
 
+// Ensure that a real <a download> click preserves the user gesture claim since
+// it legitimately has transient user activation.
+IN_PROC_BROWSER_TEST_F(DownloadContentTest, DownloadURLWithGenuineClick) {
+  GURL download_url =
+      embedded_test_server()->GetURL("/download/download-test.lib");
+
+  EXPECT_TRUE(
+      NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html")));
+  WebContentsImpl* web_contents =
+      static_cast<WebContentsImpl*>(shell()->web_contents());
+
+  EXPECT_TRUE(ExecJs(web_contents,
+                     "let a = document.createElement('a');"
+                     "a.id = 'downloadlink';"
+                     "a.download = 'download-test.lib';"
+                     "a.href = '" +
+                         download_url.spec() +
+                         "';"
+                         "a.innerText = 'click me';"
+                         "document.body.appendChild(a);"));
+
+  std::unique_ptr<DownloadTestObserver> observer(
+      CreateInProgressWaiter(shell(), 1));
+  EXPECT_TRUE(ExecJs(web_contents, "document.getElementById('downloadlink').click()"));
+
+  observer->WaitForFinished();
+
+  std::vector<raw_ptr<download::DownloadItem, VectorExperimental>> downloads;
+  DownloadManagerForShell(shell())->GetAllDownloads(&downloads);
+  ASSERT_EQ(1u, downloads.size());
+  EXPECT_TRUE(downloads[0]->HasUserGesture());
+}
+
+// Ensure that calling DownloadURL from a frame without transient user
+// activation does not preserve a spoofed has_user_gesture claim.
+IN_PROC_BROWSER_TEST_F(DownloadContentTest, DownloadURLWithoutUserActivation) {
+  GURL download_url =
+      embedded_test_server()->GetURL("/download/download-test.lib");
+
+  EXPECT_TRUE(
+      NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html")));
+  WebContentsImpl* web_contents =
+      static_cast<WebContentsImpl*>(shell()->web_contents());
+  RenderFrameHostImpl* main_frame =
+      web_contents->GetPrimaryFrameTree().root()->current_frame_host();
+
+  EXPECT_FALSE(main_frame->HasTransientUserActivation());
+
+  std::unique_ptr<DownloadTestObserver> observer(
+      CreateInProgressWaiter(shell(), 1));
+
+  auto params = blink::mojom::DownloadURLParams::New();
+  params->url = download_url;
+  params->initiator_origin = main_frame->GetLastCommittedOrigin();
+  params->has_user_gesture = true;
+  main_frame->DownloadURL(std::move(params));
+
+  observer->WaitForFinished();
+
+  std::vector<raw_ptr<download::DownloadItem, VectorExperimental>> downloads;
+  DownloadManagerForShell(shell())->GetAllDownloads(&downloads);
+  ASSERT_EQ(1u, downloads.size());
+  EXPECT_FALSE(downloads[0]->HasUserGesture());
+}
+
 using DownloadRangeTestParams =
     std::tuple<int64_t /*starting byte in range request*/,
                int64_t /*ending byte in range request*/,
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index ade25ac..19bb216 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -5701,4 +5701,33 @@
   EXPECT_FALSE(subframe->IsRenderFrameLive());
 }
 
+// Ensure that the renderer is terminated if it sends a DownloadURL IPC with a
+// referrer URL that the renderer's process is not allowed to host.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, DownloadURLInvalidReferrer) {
+  // Explicitly isolating a.com helps ensure that this test is applicable on
+  // platforms without site-per-process.
+  IsolateOrigin("a.com");
+
+  GURL main_url(embedded_test_server()->GetURL("a.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), main_url));
+
+  WebContentsImpl* web_contents =
+      static_cast<WebContentsImpl*>(shell()->web_contents());
+  RenderFrameHostImpl* main_frame =
+      web_contents->GetPrimaryFrameTree().root()->current_frame_host();
+
+  // Simulate that the renderer sends a DownloadURL IPC claiming that the
+  // download was referred by a page on a different site.
+  auto params = blink::mojom::DownloadURLParams::New();
+  params->url = embedded_test_server()->GetURL("a.com", "/title2.html");
+  params->initiator_origin = main_frame->GetLastCommittedOrigin();
+  params->referrer = blink::mojom::Referrer::New();
+  params->referrer->url =
+      embedded_test_server()->GetURL("b.com", "/title1.html");
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(main_frame->GetProcess());
+  main_frame->DownloadURL(std::move(params));
+  EXPECT_EQ(bad_message::RFH_DOWNLOAD_URL_INVALID_REFERRER, kill_waiter.Wait());
+  EXPECT_FALSE(main_frame->IsRenderFrameLive());
+}
+
 }  // namespace content
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.