CVE-2026-11242
Overview
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
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/plugins/plugin_info_host_impl.cc |
modified | |
ifchrome/renderer/chrome_content_renderer_client.cc |
modified | |
IN_PROC_BROWSER_TEST_Fchrome/browser/plugins/plugin_info_host_impl_browsertest.cc |
modified |
Files Changed
chrome/browser/plugins/plugin_info_host_impl.ccchrome/browser/plugins/plugin_info_host_impl.hchrome/browser/plugins/plugin_info_host_impl_browsertest.ccchrome/common/plugin.mojomchrome/renderer/chrome_content_renderer_client.ccchrome/renderer/plugins/chrome_plugin_placeholder.cc
Patch
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.
Regression Test / PoC
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.
Original Bug Report
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.ccchrome/browser/plugins/plugin_utils.ccchrome/common/plugin.mojomchrome/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.
- Compromise a Renderer: An attacker achieves code execution within a standard web renderer process (e.g., via a v8 exploit).
- Bind the Interface: The compromised renderer acquires a handle to the
chrome::mojom::PluginInfoHostinterface. - 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/mswordfor QuickOffice on ChromeOS). - 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. - Observe the Result: The attacker observes the returned
PluginInfo->status.kAllowedmeans JavaScript is enabled for the victim site;kBlockedmeans 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.