Low CVSS 7.5 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Plugins
DescriptionInsufficient validation of untrusted input in Plugins
ComponentPlugins
Bug ClassInput Validation
Tracker497385823
Fix commit3c9de1bd67f2 (chromium/src) +31/-33
Related commitsbad2242d8208 previously mapped (not the fix) · chromium/src
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-29 (per vendor advisory)
Fix landed2026-04-08 (public git)
Disclosed2026-06-02
Patch gap55 days — fix public in source before the advisory
Re-mapped on 2026-10-03: our pipeline had picked the earliest commit citing this bug (bad2242d8208), which an audit found was a preparatory/helper change, not the fix. This page now points to the commit whose diff removes the bug. No root-cause analysis is published for this CVE.

Changed Functions

FunctionChangeNotes
if
chrome/browser/plugins/plugin_info_host_impl.cc
modified
if
chrome/renderer/chrome_content_renderer_client.cc
modified
IN_PROC_BROWSER_TEST_F
chrome/browser/plugins/plugin_info_host_impl_browsertest.cc
modified

Files Changed

  • chrome/browser/plugins/plugin_info_host_impl.cc
  • chrome/browser/plugins/plugin_info_host_impl.h
  • chrome/browser/plugins/plugin_info_host_impl_browsertest.cc
  • chrome/common/plugin.mojom
  • chrome/renderer/chrome_content_renderer_client.cc
  • chrome/renderer/plugins/chrome_plugin_placeholder.cc
diff --git a/chrome/browser/plugins/plugin_info_host_impl.cc b/chrome/browser/plugins/plugin_info_host_impl.cc
index c859c57..52fd7df 100644
--- a/chrome/browser/plugins/plugin_info_host_impl.cc
+++ b/chrome/browser/plugins/plugin_info_host_impl.cc
@@ -190,22 +190,28 @@
 PluginInfoHostImpl::~PluginInfoHostImpl() = default;
 
 void PluginInfoHostImpl::GetPluginInfo(const GURL& url,
-                                       const url::Origin& origin,
                                        const std::string& mime_type,
                                        GetPluginInfoCallback callback) {
   DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
   // Refresh plugins.
   PluginService::GetInstance()->GetPlugins();
   chrome::mojom::PluginInfoPtr output = chrome::mojom::PluginInfo::New();
+  auto* rfh = content::RenderFrameHost::FromFrameToken(context_.rfh_token());
+  if (!rfh) {
+    std::move(callback).Run(std::move(output));
+    return;
+  }
+
   // This also fills in |actual_mime_type|.
   std::unique_ptr<PluginMetadata> plugin_metadata;
   if (context_.FindEnabledPlugin(url, mime_type, &output->status,
                                  &output->plugin, &output->actual_mime_type,
                                  &plugin_metadata)) {
     // TODO(crbug.com/40164563): Simplify this once PDF is the only "plugin."
-    context_.DecidePluginStatus(url, origin, output->plugin,
-                                plugin_metadata->security_status(),
-                                plugin_metadata->identifier(), &output->status);
+    context_.DecidePluginStatus(
+        url, rfh->GetMainFrame()->GetLastCommittedOrigin(), output->plugin,
+        plugin_metadata->security_status(), plugin_metadata->identifier(),
+        &output->status);
   }
 
   if (plugin_metadata) {
diff --git a/chrome/browser/plugins/plugin_info_host_impl.h b/chrome/browser/plugins/plugin_info_host_impl.h
index c074e00a..fb9ed6dc 100644
--- a/chrome/browser/plugins/plugin_info_host_impl.h
+++ b/chrome/browser/plugins/plugin_info_host_impl.h
@@ -61,6 +61,10 @@
     void MaybeGrantAccess(chrome::mojom::PluginStatus status,
                           const base::FilePath& path) const;
 
+    const content::GlobalRenderFrameHostToken& rfh_token() const {
+      return rfh_token_;
+    }
+
    private:
     const content::GlobalRenderFrameHostToken rfh_token_;
 #if BUILDFLAG(ENABLE_EXTENSIONS)
@@ -81,7 +85,6 @@
 
   // chrome::mojom::PluginInfoHost
   void GetPluginInfo(const GURL& url,
-                     const url::Origin& origin,
                      const std::string& mime_type,
                      GetPluginInfoCallback callback) override;
 
diff --git a/chrome/common/plugin.mojom b/chrome/common/plugin.mojom
index 7732c887..ccc7cb7 100644
--- a/chrome/common/plugin.mojom
+++ b/chrome/common/plugin.mojom
@@ -55,7 +55,5 @@
   // Includes specific reasons why a plugin can't be used, for example because
   // it's disabled.
   [Sync]
-  GetPluginInfo(url.mojom.Url url,
-                url.mojom.Origin origin,
-                string mime_type) => (PluginInfo plugin_info);
+  GetPluginInfo(url.mojom.Url url, string mime_type) => (PluginInfo info);
 };
diff --git a/chrome/renderer/chrome_content_renderer_client.cc b/chrome/renderer/chrome_content_renderer_client.cc
index d8a0da07..5c64a8de 100644
--- a/chrome/renderer/chrome_content_renderer_client.cc
+++ b/chrome/renderer/chrome_content_renderer_client.cc
@@ -829,9 +829,7 @@
   // not supported. Here it suffices to return false but there should perhaps be
   // a more unified approach to avoid sending the IPC twice.
   chrome::mojom::PluginInfoPtr plugin_info = chrome::mojom::PluginInfo::New();
-  plugin_info_host->GetPluginInfo(
-      original_url, render_frame->GetWebFrame()->Top()->GetSecurityOrigin(),
-      mime_type, &plugin_info);
+  plugin_info_host->GetPluginInfo(original_url, mime_type, &plugin_info);
   // TODO(ekaramad): Not continuing here due to a disallowed status should take
   // us to CreatePlugin. See if more in depths investigation of |status| is
   // necessary here (see https://crbug.com/41460326). For now, returning false
@@ -924,9 +922,7 @@
       &plugin_info_host);
 
   chrome::mojom::PluginInfoPtr plugin_info = chrome::mojom::PluginInfo::New();
-  plugin_info_host->GetPluginInfo(
-      url, render_frame->GetWebFrame()->Top()->GetSecurityOrigin(),
-      orig_mime_type, &plugin_info);
+  plugin_info_host->GetPluginInfo(url, orig_mime_type, &plugin_info);
   *plugin = CreatePlugin(render_frame, params, *plugin_info);
 #else  // !BUILDFLAG(ENABLE_PLUGINS)
   if (orig_mime_type == pdf::kPDFMimeType) {
diff --git a/chrome/renderer/plugins/chrome_plugin_placeholder.cc b/chrome/renderer/plugins/chrome_plugin_placeholder.cc
index 24312e4c..ff97ba0 100644
--- a/chrome/renderer/plugins/chrome_plugin_placeholder.cc
+++ b/chrome/renderer/plugins/chrome_plugin_placeholder.cc
@@ -224,10 +224,8 @@
   mojo::AssociatedRemote<chrome::mojom::PluginInfoHost> plugin_info_host;
   render_frame()->GetRemoteAssociatedInterfaces()->GetInterface(
       &plugin_info_host);
-  plugin_info_host->GetPluginInfo(
-      GetPluginParams().url,
-      render_frame()->GetWebFrame()->Top()->GetSecurityOrigin(), mime_type,
-      &plugin_info);
+  plugin_info_host->GetPluginInfo(GetPluginParams().url, mime_type,
+                                  &plugin_info);
   if (plugin_info->status == status_)
     return;
   blink::WebPlugin* new_plugin = ChromeContentRendererClient::CreatePlugin(
diff --git a/chrome/browser/plugins/plugin_info_host_impl_browsertest.cc b/chrome/browser/plugins/plugin_info_host_impl_browsertest.cc
index cd7d828..a185541 100644
--- a/chrome/browser/plugins/plugin_info_host_impl_browsertest.cc
+++ b/chrome/browser/plugins/plugin_info_host_impl_browsertest.cc
@@ -72,9 +72,7 @@
   void TearDownOnMainThread() override { plugin_info_host_impl_.reset(); }
 
  protected:
-  PluginInfoPtr GetPluginInfo(const GURL& url,
-                              const url::Origin& origin,
-                              const std::string& mime_type) {
+  PluginInfoPtr GetPluginInfo(const GURL& url, const std::string& mime_type) {
     PluginInfoPtr plugin_info;
 
     base::MockCallback<PluginInfoHost::GetPluginInfoCallback> mock_callback;
@@ -82,8 +80,7 @@
 
     base::RunLoop run_loop;
     plugin_info_host_impl_->GetPluginInfo(
-        url, origin, mime_type,
-        mock_callback.Get().Then(run_loop.QuitClosure()));
+        url, mime_type, mock_callback.Get().Then(run_loop.QuitClosure()));
     run_loop.Run();
 
     return plugin_info;
@@ -153,16 +150,16 @@
 }
 
 IN_PROC_BROWSER_TEST_F(PluginInfoHostImplTest, GetPluginInfoForFlash) {
-  PluginInfoPtr plugin_info = GetPluginInfo(GURL("fake.swf"), url::Origin(),
-                                            "application/x-shockwave-flash");
+  PluginInfoPtr plugin_info =
+      GetPluginInfo(GURL("fake.swf"), "application/x-shockwave-flash");
   ASSERT_TRUE(plugin_info);
 
   EXPECT_EQ(PluginStatus::kNotFound, plugin_info->status);
 }
 
 IN_PROC_BROWSER_TEST_F(PluginInfoHostImplTest, GetPluginInfoForFutureSplash) {
-  PluginInfoPtr plugin_info = GetPluginInfo(GURL("fake.spl"), url::Origin(),
-                                            "application/futuresplash");
+  PluginInfoPtr plugin_info =
+      GetPluginInfo(GURL("fake.spl"), "application/futuresplash");
   ASSERT_TRUE(plugin_info);
 
   EXPECT_EQ(PluginStatus::kNotFound, plugin_info->status);
@@ -180,7 +177,7 @@
 #endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
 
   PluginInfoPtr plugin_info =
-      GetPluginInfo(GURL("fake.pdf"), url::Origin(), pdf::kPDFMimeType);
+      GetPluginInfo(GURL("fake.pdf"), pdf::kPDFMimeType);
   ASSERT_TRUE(plugin_info);
 
   EXPECT_EQ(PluginStatus::kAllowed, plugin_info->status);
@@ -224,7 +221,7 @@
   SetAlwaysOpenPdfExternally();
 
   PluginInfoPtr plugin_info =
-      GetPluginInfo(GURL("fake.pdf"), url::Origin(), pdf::kPDFMimeType);
+      GetPluginInfo(GURL("fake.pdf"), pdf::kPDFMimeType);
   ASSERT_TRUE(plugin_info);
 
   // PDF viewer extension is disabled by PDF content setting.
@@ -242,8 +239,8 @@
   const std::string kGroupId = "chromium-pdf-plugin";
 #endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
 
-  PluginInfoPtr plugin_info = GetPluginInfo(GURL("fake.pdf"), url::Origin(),
-                                            pdf::kInternalPluginMimeType);
+  PluginInfoPtr plugin_info =
+      GetPluginInfo(GURL("fake.pdf"), pdf::kInternalPluginMimeType);
   ASSERT_TRUE(plugin_info);
 
   EXPECT_EQ(PluginStatus::kAllowed, plugin_info->status);
@@ -278,8 +275,8 @@
                        GetPluginInfoForPdfInternalPluginWhenDisabled) {
   SetAlwaysOpenPdfExternally();
 
-  PluginInfoPtr plugin_info = GetPluginInfo(GURL("fake.pdf"), url::Origin(),
-                                            pdf::kInternalPluginMimeType);
+  PluginInfoPtr plugin_info =
+      GetPluginInfo(GURL("fake.pdf"), pdf::kInternalPluginMimeType);
   ASSERT_TRUE(plugin_info);
 
   // Internal PDF plugin is not affected by PDF content setting.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/plugins/plugin_info_host_impl_browsertest.cc b/chrome/browser/plugins/plugin_info_host_impl_browsertest.cc
index cd7d828..a185541 100644
--- a/chrome/browser/plugins/plugin_info_host_impl_browsertest.cc
+++ b/chrome/browser/plugins/plugin_info_host_impl_browsertest.cc
@@ -72,9 +72,7 @@
   void TearDownOnMainThread() override { plugin_info_host_impl_.reset(); }
 
  protected:
-  PluginInfoPtr GetPluginInfo(const GURL& url,
-                              const url::Origin& origin,
-                              const std::string& mime_type) {
+  PluginInfoPtr GetPluginInfo(const GURL& url, const std::string& mime_type) {
     PluginInfoPtr plugin_info;
 
     base::MockCallback<PluginInfoHost::GetPluginInfoCallback> mock_callback;
@@ -82,8 +80,7 @@
 
     base::RunLoop run_loop;
     plugin_info_host_impl_->GetPluginInfo(
-        url, origin, mime_type,
-        mock_callback.Get().Then(run_loop.QuitClosure()));
+        url, mime_type, mock_callback.Get().Then(run_loop.QuitClosure()));
     run_loop.Run();
 
     return plugin_info;
@@ -153,16 +150,16 @@
 }
 
 IN_PROC_BROWSER_TEST_F(PluginInfoHostImplTest, GetPluginInfoForFlash) {
-  PluginInfoPtr plugin_info = GetPluginInfo(GURL("fake.swf"), url::Origin(),
-                                            "application/x-shockwave-flash");
+  PluginInfoPtr plugin_info =
+      GetPluginInfo(GURL("fake.swf"), "application/x-shockwave-flash");
   ASSERT_TRUE(plugin_info);
 
   EXPECT_EQ(PluginStatus::kNotFound, plugin_info->status);
 }
 
 IN_PROC_BROWSER_TEST_F(PluginInfoHostImplTest, GetPluginInfoForFutureSplash) {
-  PluginInfoPtr plugin_info = GetPluginInfo(GURL("fake.spl"), url::Origin(),
-                                            "application/futuresplash");
+  PluginInfoPtr plugin_info =
+      GetPluginInfo(GURL("fake.spl"), "application/futuresplash");
   ASSERT_TRUE(plugin_info);
 
   EXPECT_EQ(PluginStatus::kNotFound, plugin_info->status);
@@ -180,7 +177,7 @@
 #endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
 
   PluginInfoPtr plugin_info =
-      GetPluginInfo(GURL("fake.pdf"), url::Origin(), pdf::kPDFMimeType);
+      GetPluginInfo(GURL("fake.pdf"), pdf::kPDFMimeType);
   ASSERT_TRUE(plugin_info);
 
   EXPECT_EQ(PluginStatus::kAllowed, plugin_info->status);
@@ -224,7 +221,7 @@
   SetAlwaysOpenPdfExternally();
 
   PluginInfoPtr plugin_info =
-      GetPluginInfo(GURL("fake.pdf"), url::Origin(), pdf::kPDFMimeType);
+      GetPluginInfo(GURL("fake.pdf"), pdf::kPDFMimeType);
   ASSERT_TRUE(plugin_info);
 
   // PDF viewer extension is disabled by PDF content setting.
@@ -242,8 +239,8 @@
   const std::string kGroupId = "chromium-pdf-plugin";
 #endif  // BUILDFLAG(GOOGLE_CHROME_BRANDING)
 
-  PluginInfoPtr plugin_info = GetPluginInfo(GURL("fake.pdf"), url::Origin(),
-                                            pdf::kInternalPluginMimeType);
+  PluginInfoPtr plugin_info =
+      GetPluginInfo(GURL("fake.pdf"), pdf::kInternalPluginMimeType);
   ASSERT_TRUE(plugin_info);
 
   EXPECT_EQ(PluginStatus::kAllowed, plugin_info->status);
@@ -278,8 +275,8 @@
                        GetPluginInfoForPdfInternalPluginWhenDisabled) {
   SetAlwaysOpenPdfExternally();
 
-  PluginInfoPtr plugin_info = GetPluginInfo(GURL("fake.pdf"), url::Origin(),
-                                            pdf::kInternalPluginMimeType);
+  PluginInfoPtr plugin_info =
+      GetPluginInfo(GURL("fake.pdf"), pdf::kInternalPluginMimeType);
   ASSERT_TRUE(plugin_info);
 
   // Internal PDF plugin is not affected by PDF content setting.
Loading diff…

Original Bug Report

reported by [email protected]

Cross-origin info leak of JS settings via PluginInfoHost::GetPluginInfo

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: The PluginInfoHost::GetPluginInfo IPC accepts an unvalidated origin parameter from the renderer. A compromised renderer can spoof this origin when querying for non-fully-trusted plugins (like QuickOffice) to leak the user’s site-specific JavaScript content settings, bypassing Site Isolation.

Affected files:

  • chrome/browser/plugins/plugin_info_host_impl.cc
  • chrome/browser/plugins/plugin_utils.cc
  • chrome/common/plugin.mojom
  • chrome/browser/plugins/chrome_content_browser_client_plugins_part.cc

Estimated timestamp from git blame: 2025-11-13

Description

In Chrome’s plugin architecture, the chrome::mojom::PluginInfoHost interface allows renderer processes to query the status of plugins for a given URL and MIME type. The GetPluginInfo method accepts a url::Origin parameter, which is supposed to represent the origin of the main frame loading the plugin.

However, PluginInfoHostImpl::GetPluginInfo (in chrome/browser/plugins/plugin_info_host_impl.cc) does not validate that the provided origin matches the actual committed origin of the requesting frame. It passes the renderer-supplied origin directly to Context::DecidePluginStatus, which then calls PluginUtils::GetPluginContentSetting.

PluginUtils::GetPluginContentSetting uses this spoofed origin to query the HostContentSettingsMap for the user’s ContentSettingsType::JAVASCRIPT setting. If the queried plugin is not marked as SECURITY_STATUS_FULLY_TRUSTED (e.g., the QuickOffice component extension on ChromeOS), the result of this JavaScript content setting lookup directly dictates the PluginStatus returned to the renderer (kAllowed vs kBlocked).

Impact

A compromised renderer process can exploit this to systematically fingerprint a user’s browser configuration. By rapidly querying the IPC with various target origins, the attacker can map out exactly which sites the user has explicitly allowed or blocked from running JavaScript, constituting a cross-origin information leak.

Potential Exploitation Steps

Note: These are suggested steps based on code analysis; our tooling has not yet executed a live Proof of Concept.

  1. Compromise a Renderer: An attacker achieves code execution within a standard web renderer process (e.g., via a v8 exploit).
  2. Bind the Interface: The compromised renderer acquires a handle to the chrome::mojom::PluginInfoHost interface.
  3. Spoof the Origin: The attacker invokes GetPluginInfo, passing a target origin (e.g., https://victim-bank.com) and a MIME type handled by a non-fully-trusted plugin (e.g., application/msword for QuickOffice on ChromeOS).
  4. Extract the Setting: The browser process processes the IPC without validation, checks the JavaScript content setting for https://victim-bank.com, and returns the result.
  5. Observe the Result: The attacker observes the returned PluginInfo->status. kAllowed means JavaScript is enabled for the victim site; kBlocked means it is disabled.

Suggested Fix

The PluginInfoHost should not trust the origin provided by the renderer process. Currently, PluginInfoHostImpl is instantiated with only the render_process_id.

To fix this, the interface binding in ChromeContentBrowserClientPluginsPart::ExposeInterfacesToRendererForRenderFrameHost should be updated to pass the content::GlobalRenderFrameHostId instead of just the process ID. Then, PluginInfoHostImpl::GetPluginInfo can securely retrieve the RenderFrameHost and use render_frame_host->GetLastCommittedOrigin() (or validate the IPC-provided origin against it) before performing any content setting lookups.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from 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