Medium watch CVSS 8.3 chrome Policy Bypass 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Extensions
DescriptionInsufficient policy enforcement in Extensions
ComponentExtensions
Bug ClassPolicy Bypass
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.
Tracker511772271
Fix commitf66e7ae1e551 (chromium/src) +110/-1
CWECWE-807
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-10 (per vendor advisory)
Fix landed2026-08-11 (public git)
Disclosed2026-09-08
Patch gap28 days — fix public in source before the advisory

Background

`DownloadURL` IPC
a renderer-to-browser message carrying blink::mojom::DownloadURLParams (URL, initiator_origin, referrer, has_user_gesture) that asks the browser process to start a download.
`HostsOrigin`
a ChildProcessSecurityPolicyImpl check that confirms a given renderer process is actually allowed to host a particular url::Origin, used to reject renderer-supplied URLs that the process could not legitimately have loaded.
Transient user activation
a short-lived, browser-tracked signal (queried via HasTransientUserActivation()) recording that a frame recently received a genuine user gesture; it gates behaviors that must not be script-driven.
Off-store install policy
the extensions gate (referenced in the commit message as IsOffstoreInstallAllowed) that decides whether an extension may be installed from outside the Web Store, keyed in part on the download’s referrer and user-gesture provenance.

Root Cause Analysis

Before the patch, the browser process accepted the referrer and has_user_gesture fields of a DownloadURL IPC on trust from the renderer. In ipc_utils.cc there was no verification that the renderer’s process was entitled to host the origin of params.referrer->url, so a compromised or malicious renderer could attach a referrer pointing at any site it did not actually host. Separately, in the DownloadURL handling in render_frame_host_impl.cc, the code did parameters->set_has_user_gesture(blink_parameters->has_user_gesture), propagating the renderer’s self-reported gesture claim verbatim without confirming the frame really had activation. This violated the invariant that security-relevant provenance fields crossing the renderer/browser trust boundary must be independently validated in the browser.

The fix adds a HostsOrigin check that terminates the renderer (RFH_DOWNLOAD_URL_INVALID_REFERRER) when the claimed referrer origin is not hosted by that process, and rewrites the gesture assignment to blink_parameters->has_user_gesture && HasTransientUserActivation() so the browser’s own activation state is authoritative. Both changes are added guards only — the patch tightens validation and does not alter the downstream download or install mechanism itself.

Key insight
The core mistake was trusting renderer-supplied provenance (referrer and has_user_gesture) as authoritative for a download whose provenance feeds a policy decision; the fix makes the browser re-derive both facts itself — hosting the referrer origin via HostsOrigin, and gating the gesture on HasTransientUserActivation().

Attack Path

  1. Reach the IPC from a controlled renderer An attacker with script execution in a renderer (or a compromised renderer) constructs a blink::mojom::DownloadURLParams and sends it via the DownloadURL IPC, as the added SecurityExploitBrowserTest.DownloadURLInvalidReferrer test does directly.
  2. Spoof the referrer The attacker sets params.referrer->url to an origin its process does not host (the test uses b.com from an a.com frame); pre-patch, ipc_utils.cc did not call HostsOrigin, so the forged referrer was accepted by the browser.
  3. Spoof the user gesture The attacker sets params.has_user_gesture = true from a frame that has no transient activation (the added DownloadURLWithoutUserActivation test asserts HasTransientUserActivation() is false yet the claim was true); pre-patch the browser copied this claim through unchanged.
  4. Bypass the off-store install policy With a forged allowlisted referrer and a fabricated gesture, the download’s provenance can satisfy the extensions off-store install gate that would otherwise block it, matching the advisory’s “Insufficient policy enforcement in Extensions” (inferred that this is the specific downstream policy, per the commit message’s reference to IsOffstoreInstallAllowed).
  5. Constraint noted by the fix author The commit message states IsOffstoreInstallAllowed also requires the authoritative download URL to match the allowlist, so a spoofed referrer alone does not complete the bypass; the full policy bypass beyond what these two checks demonstrate is (inferred).

Impact Assessment

An attacker controlling a renderer can forge the referrer and user-gesture provenance of a browser-process download to weaken policy checks, specifically the extensions off-store install enforcement, corresponding to the advisory’s “Insufficient policy enforcement in Extensions” (PolicyBypass). The precondition is the ability to send a crafted DownloadURL IPC — i.e., script execution in or compromise of a renderer process. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.3. Any escalation beyond bypassing the download-provenance policy checks the patch adds (e.g., unwanted extension installation or further compromise) is (inferred).

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/download/download_browsertest.cc
  • tools/metrics/histograms/metadata/stability/enums.xml
  • content/browser/bad_message.h
  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/browser/renderer_host/ipc_utils.cc
  • content/browser/security_exploit_browsertest.cc

Audit Directions

  • Renderer-supplied provenance fields
    Audit other IPCs whose referrer, initiator_origin, or gesture/activation fields are consumed by the browser without a HostsOrigin/VerifyInitiatorOrigin/activation re-check, since those fields cross the trust boundary and may feed policy decisions.
  • Self-reported user-gesture claims
    Look for any browser-side code that copies a renderer’s has_user_gesture (or equivalent activation boolean) without confirming it against HasTransientUserActivation(), especially where the value gates security-sensitive actions like installs, popups, or downloads.
  • Policy inputs derived from downloads
    Trace where download parameters influence extension install allowlists or similar gates (e.g., IsOffstoreInstallAllowed) and verify each provenance input is browser-authoritative and not merely a match against a spoofable renderer-supplied value.
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/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/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">
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…

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

reported by [email protected]

ExtensionInstallSources bypass via forged DownloadURLParams

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A compromised renderer can spoof the referrer and has_user_gesture fields in the DownloadURL Mojo IPC. The browser trusts these parameters without authoritative validation, allowing an attacker to bypass the ExtensionInstallSources policy and Safe Browsing warnings to prompt the user to install a malicious, self-signed extension.

Affected files:

  • content/browser/renderer_host/render_frame_host_impl.cc
  • chrome/browser/extensions/extension_management.cc
  • chrome/browser/download/download_target_determiner.cc
  • extensions/browser/crx_installer.cc
  • chrome/browser/download/download_crx_util.cc
  • content/browser/renderer_host/ipc_utils.cc

Estimated timestamp from git blame: 2024-12-10

Vulnerability Details

The browser process exposes the LocalFrameHost::DownloadURL Mojo IPC method to renderer processes. When a renderer sends a blink::mojom::DownloadURLParams message, RenderFrameHostImpl::DownloadURL processes it and calls VerifyDownloadUrlParams (content/browser/renderer_host/ipc_utils.cc). While this function validates elements like the blob token and initiator origin, it does not validate the referrer or has_user_gesture fields against authoritative browser-side state.

These unverified fields are copied into a download::DownloadUrlParameters object and passed to the download subsystem. This creates a critical security boundary violation when enterprise policies are in use:

  1. Warning Suppression: During download processing, DownloadTargetDeterminer::GetDangerLevel evaluates if the download is dangerous. If download_->HasUserGesture() is true and the download is deemed a trusted extension (checked via ExtensionManagement::IsOffstoreInstallAllowed), it returns DownloadFileType::NOT_DANGEROUS. This entirely suppresses Safe Browsing dangerous file warnings.
  2. Policy Bypass: ExtensionManagement::IsOffstoreInstallAllowed checks if the referrer matches the ExtensionInstallSources enterprise policy. Because the referrer is fully controlled by the compromised renderer, an attacker can spoof a trusted domain (e.g., https://trusted.example.corp/).
  3. Signature Proof Bypass: Once the download completes, ChromeDownloadManagerDelegate::ShouldOpenDownload checks the allowed status again. Because the spoofed referrer satisfies the policy check, CrxInstaller is initialized with OffStoreInstallAllowedBecausePref. This causes CrxInstaller::InstallCrx to use VerifierFormat::CRX3 (which only requires a standard cryptographic signature) instead of VerifierFormat::CRX3_WITH_PUBLISHER_PROOF (which requires Google Web Store keys).

As a result, a compromised renderer can force the browser to display a legitimate-looking “Add extension?” permissions prompt for an arbitrary, attacker-signed .crx file, bypassing strict enterprise controls.

Potential Reproduction Steps

Note: Our tooling agent does not currently have the ability to run code, so these are suggested/potential steps based on static analysis of the Chromium codebase.

  1. Assume a target browser is configured with an ExtensionInstallSources enterprise policy (e.g., ["https://trusted.domain/*"]).
  2. The attacker hosts a malicious, self-signed .crx extension file.
  3. The attacker compromises a renderer process (e.g., via a V8 vulnerability).
  4. From the compromised renderer, the attacker sends a LocalFrameHost::DownloadURL Mojo IPC to the browser.
  5. In the blink::mojom::DownloadURLParams payload, the attacker sets:
    • url: The location of the malicious .crx.
    • referrer: Spoofed to https://trusted.domain/.
    • has_user_gesture: Spoofed to true.
  6. The browser receives the IPC, trusts the forged parameters, suppresses Safe Browsing warnings, and presents the user with a standard extension installation prompt. If the user accepts, the malicious extension is installed with requested privileges.

Suggested Fix

The browser process must not rely on renderer-provided data for security decisions (CWE-807).

  1. In RenderFrameHostImpl::DownloadURL, validate the has_user_gesture parameter against the browser’s authoritative state (e.g., by checking HasTransientUserActivation()). If the renderer claims a gesture but the browser disagrees, the gesture claim should be dropped or the IPC terminated.
  2. Similarly, validate the referrer URL against GetLastCommittedOrigin() or GetLastCommittedURL() for the given frame, ensuring a compromised renderer cannot claim a referrer outside of its actual origin.

Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955


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. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker