Medium watch ✔︎ verified CVSS 6.5 chrome Info Disclosure 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Extensions
DescriptionInformation leak in Extensions
ComponentExtensions
Bug ClassInfo Disclosure
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar — factors set by independent review by AI
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.
Independent review by AIConfirmed
Reviewer notes: none false; citations :241-243, :294-299, :327 match. Omits that commit message says chrome.mimeHandler is gated by channel/feature-flag and requires installing the extension. Mapped 77d1561 is a prerequisite refactor; 82b93e5 is the fix.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker533917984
Fix commit82b93e578f4f (chromium/src) +120/-0
Related commits77d1561238e8 previously mapped (not the fix) · chromium/src
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedOran Simhony from Palo Alto Networks
Reported2026-07-12 (per vendor advisory)
Fix landed2026-07-15 (public git)
Disclosed2026-08-25
Patch gap41 days — fix public in source before the advisory

Background

MIME handler extension
an extension that registers via MimeTypesHandler to handle a MIME type such as application/pdf; a third-party application/pdf handler needs no permissions and shows no install warning.
`PluginResponseInterceptorURLLoaderThrottle`
the browser-process throttle that intercepts a response bound for a plugin/MIME handler and hands the handler a copy of the response head.
CORS-safelisted response header name
the small set of response headers (e.g. content-type, cache-control, last-modified) that fetch() exposes to script cross-origin.

Root Cause Analysis

Re-mapped on 2026-10-03: our pipeline had picked the earliest commit citing this bug (77d1561238e8), which an audit found was a preparatory/helper change, not the fix. This page now points to the commit whose diff removes the bug.

In PluginResponseInterceptorURLLoaderThrottle::WillProcessResponse, the throttle determines is_for_generic_mime_handler from MimeTypesHandler::IsPluginExtension() (pre-fix plugin_response_interceptor_url_loader_throttle.cc:241-243) and later deep-copies the response head, rebuilding a full copy of all headers via base::MakeRefCounted<net::HttpResponseHeaders>(response_head->headers->raw_headers()) (:294-299). That copy is placed in transferrable_loader->head (:327) and handed to the extension, which can read it back through chrome.mimeHandler.getStreamInfo(). Pre-fix nothing restricted this extension-facing header set, so a zero-permission generic handler saw every response header, including cross-origin-sensitive ones such as X-Auth-Token, that it could never obtain through fetch() without host permissions. The invariant that an extension should only see the response headers it is otherwise entitled to (the CORS-safelisted set) was violated.

The fix adds the helper FilterToCorsSafelistedResponseHeaders() and, guarded by if (is_for_generic_mime_handler && deep_copied_response->headers), strips the deep-copied headers down to names accepted by network::cors::IsCorsSafelistedResponseHeaderName() before the copy is forwarded; trusted plugin handlers and the original response_head are left unfiltered.

The patch TIGHTENS what a generic handler receives.

Key insight
The interceptor handed third-party MIME handlers an unfiltered copy of all cross-origin response headers; the fix restricts that extension-facing copy to the CORS-safelisted response-header names for generic handlers only.

Attack Path

  1. Install a zero-permission PDF handler Ship an extension that registers as the application/pdf MIME handler, which requires no permissions and triggers no install warning.
  2. User opens a cross-origin PDF The user navigates to any origin that serves a PDF, invoking the plugin response interceptor for the generic handler.
  3. Read leaked headers The handler calls chrome.mimeHandler.getStreamInfo() and reads responseHeaders, which pre-fix included non-safelisted cross-origin headers such as X-Auth-Token.

Impact Assessment

A no-permission extension could read arbitrary cross-origin HTTP response headers (auth tokens, CSRF tokens, internal headers) for any PDF the user opens from any origin — an information disclosure surfaced through the browser process’ extension-facing response stream. The advisory rates this medium severity, classifies it as an information leak, and states it was not exploited in the wild.

Changed Functions

FunctionChangeNotes
if
chrome/browser/plugins/plugin_response_interceptor_url_loader_throttle.cc
modified
if
chrome/browser/extensions/api/mime_handlers/generic_mime_handler_browsertest.cc
modified

Files Changed

  • chrome/browser/extensions/api/mime_handlers/generic_mime_handler_browsertest.cc
  • chrome/browser/plugins/plugin_response_interceptor_url_loader_throttle.cc

Audit Directions

  • Extension-facing response copies
    Flag code that forwards a full net::HttpResponseHeaders to a renderer or extension without restricting it to an explicit exposure policy.
  • Permissionless handler reach
    Audit features reachable by no-permission extensions (MIME handlers, default handlers) for cross-origin data they may expose at runtime.
diff --git a/chrome/browser/plugins/plugin_response_interceptor_url_loader_throttle.cc b/chrome/browser/plugins/plugin_response_interceptor_url_loader_throttle.cc
index 7b53b59..fe31452 100644
--- a/chrome/browser/plugins/plugin_response_interceptor_url_loader_throttle.cc
+++ b/chrome/browser/plugins/plugin_response_interceptor_url_loader_throttle.cc
@@ -32,7 +32,9 @@
 #include "mojo/public/cpp/bindings/remote.h"
 #include "mojo/public/cpp/bindings/self_owned_receiver.h"
 #include "mojo/public/cpp/system/data_pipe.h"
+#include "net/http/http_response_headers.h"
 #include "pdf/buildflags.h"
+#include "services/network/public/cpp/cors/cors.h"
 #include "services/network/public/mojom/content_security_policy.mojom.h"
 #include "services/network/public/mojom/url_loader.mojom.h"
 #include "services/network/public/mojom/url_response_head.mojom.h"
@@ -97,6 +99,25 @@
   csp.swap(cleared);
 }
 
+// Restricts `headers` to the CORS-safelisted response header names, so a
+// generic (third-party) MIME handler extension only sees the response headers
+// that fetch() would expose to script cross-origin. Without this, a
+// zero-permission handler could read arbitrary cross-origin response headers
+// (auth tokens, and similar) off the stream it handles.
+// https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name
+void FilterToCorsSafelistedResponseHeaders(net::HttpResponseHeaders* headers) {
+  std::vector<std::string> names_to_remove;
+  size_t iter = 0;
+  std::string name;
+  std::string value;
+  while (headers->EnumerateHeaderLines(&iter, &name, &value)) {
+    if (!network::cors::IsCorsSafelistedResponseHeaderName(name)) {
+      names_to_remove.emplace_back(name);
+    }
+  }
+  headers->RemoveHeaders(names_to_remove);
+}
+
 // A no-op `network::mojom::URLLoader` used on the cached-body fallback
 // path. The browser synthesizes a complete response (a buffered body
 // pipe + a synchronous `OnComplete`) for the renderer; the renderer
@@ -298,6 +319,17 @@
             response_head->headers->raw_headers());
   }
 
+  // These deep-copied headers are the extension-facing view of the response
+  // (read back via getStreamInfo). Restrict a generic (third-party) handler to
+  // the CORS-safelisted response header names so it cannot read cross-origin
+  // response headers it would never see through fetch(). Trusted handlers
+  // (allowlisted plugin extensions) are exempt: their rendering paths
+  // legitimately consume non-safelisted headers, and `response_head` itself is
+  // never filtered.
+  if (is_for_generic_mime_handler && deep_copied_response->headers) {
+    FilterToCorsSafelistedResponseHeaders(deep_copied_response->headers.get());
+  }
+
   // Save the original MIME type before any overrides. This is passed to
   // SendExecuteMimeTypeHandlerEvent for handler URL resolution.
   const std::string original_mime_type = response_head->mime_type;
diff --git a/chrome/browser/extensions/api/mime_handlers/generic_mime_handler_browsertest.cc b/chrome/browser/extensions/api/mime_handlers/generic_mime_handler_browsertest.cc
index 260fc43..2efb6e2 100644
--- a/chrome/browser/extensions/api/mime_handlers/generic_mime_handler_browsertest.cc
+++ b/chrome/browser/extensions/api/mime_handlers/generic_mime_handler_browsertest.cc
@@ -2,11 +2,15 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <memory>
 #include <string>
 
 #include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/functional/bind.h"
 #include "base/path_service.h"
 #include "base/test/scoped_feature_list.h"
+#include "base/threading/thread_restrictions.h"
 #include "chrome/browser/content_settings/host_content_settings_map_factory.h"
 #include "chrome/browser/extensions/extension_apitest.h"
 #include "chrome/browser/plugins/plugin_utils.h"
@@ -36,6 +40,9 @@
 #include "extensions/common/manifest_handlers/mime_types_handler.h"
 #include "extensions/test/result_catcher.h"
 #include "net/dns/mock_host_resolver.h"
+#include "net/http/http_status_code.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/test/embedded_test_server/http_response.h"
 #include "services/network/public/cpp/permissions_policy/permissions_policy_features.h"
 #include "ui/base/page_transition_types.h"
 #include "url/url_constants.h"
@@ -47,6 +54,34 @@
 constexpr char kPdfMimeType[] = "application/pdf";
 constexpr char kTestExtensionDir[] = "generic_mime_handler";
 constexpr char kTestPdfPath[] = "/test.pdf";
+constexpr char kHeaderProbePdfPath[] = "/header-probe.pdf";
+constexpr char kAuthTokenHeaderName[] = "X-Auth-Token";
+
+// Serves `pdf_body` at kHeaderProbePdfPath with a mix of response headers:
+// CORS-safelisted ones (Content-Type via set_content_type, plus Cache-Control
+// and Last-Modified) that a generic handler must still see, and a
+// non-safelisted X-Auth-Token that must be filtered out. `pdf_body` is the
+// real test.pdf, so the response body is a valid document rather than an
+// ad-hoc literal (the test inspects only headers, so its exact bytes do not
+// matter, but a valid PDF keeps the fixture honest).
+std::unique_ptr<net::test_server::HttpResponse>
+HandlePdfWithExtraResponseHeaders(
+    const std::string& pdf_body,
+    const net::test_server::HttpRequest& request) {
+  // RegisterRequestHandler invokes every registered handler in turn; returning
+  // nullptr for any other path lets the static-file handlers serve it.
+  if (request.relative_url != kHeaderProbePdfPath) {
+    return nullptr;
+  }
+  auto response = std::make_unique<net::test_server::BasicHttpResponse>();
+  response->set_code(net::HTTP_OK);
+  response->set_content_type(kPdfMimeType);
+  response->AddCustomHeader("Cache-Control", "max-age=0");
+  response->AddCustomHeader("Last-Modified", "Wed, 21 Oct 2026 07:28:00 GMT");
+  response->AddCustomHeader(kAuthTokenHeaderName, "s3cr3t");
+  response->set_content(pdf_body);
+  return response;
+}
 
 }  // namespace
 
@@ -71,6 +106,17 @@
         test_data_dir.AppendASCII("pdf"));
     embedded_test_server()->ServeFilesFromDirectory(
         test_data_dir_.AppendASCII(kTestExtensionDir));
+    // Reuse the shared test PDF as the probe response body, so it is a real,
+    // valid document.
+    std::string pdf_body;
+    {
+      base::ScopedAllowBlockingForTesting allow_blocking;
+      ASSERT_TRUE(base::ReadFileToString(
+          test_data_dir_.AppendASCII(kTestExtensionDir).AppendASCII("test.pdf"),
+          &pdf_body));
+    }
+    embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
+        &HandlePdfWithExtraResponseHeaders, std::move(pdf_body)));
     ASSERT_TRUE(StartEmbeddedTestServer());
   }
 
@@ -150,6 +196,48 @@
   ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();
 }
 
+// A generic (third-party) MIME handler sees exactly the CORS-safelisted
+// response headers fetch() would expose cross-origin: the safelisted
+// Content-Type / Cache-Control / Last-Modified stay visible, while the custom
+// X-Auth-Token is filtered out. handler.js already asserts mimeType and the
+// %PDF- body on this same navigation, so this test covers only responseHeaders.
+IN_PROC_BROWSER_TEST_F(GenericMimeHandlerBrowserTest,
+                       GetStreamInfoFiltersNonSafelistedResponseHeaders) {
+  ASSERT_TRUE(LoadExtension(test_data_dir_.AppendASCII(kTestExtensionDir)));
+
+  ResultCatcher catcher;
+  ASSERT_TRUE(
+      NavigateToURL(GetActiveWebContents(),
+                    embedded_test_server()->GetURL(kHeaderProbePdfPath)));
+  ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();
+
+  content::RenderFrameHost* extension_frame = FindMimeHandlerExtensionFrame();
+  ASSERT_TRUE(extension_frame);
+
+  // Whether getStreamInfo()'s responseHeaders contains `name`
+  // (case-insensitive).
+  auto has_header = [&](const char* name) {
+    return content::EvalJs(
+        extension_frame,
+        content::JsReplace(
+            "chrome.mimeHandler.getStreamInfo().then(info =>"
+            "  Object.keys(info.responseHeaders)"
+            "    .some(k => k.toLowerCase() === $1.toLowerCase()))",
+            name));
+  };
+
+  // Every CORS-safelisted response header the server sent must stay visible.
+  // These positive checks are also the vacuity guard: were responseHeaders
+  // ever empty or getStreamInfo() to fail, they would fail here instead of
+  // letting the X-Auth-Token check below pass against an empty object.
+  EXPECT_EQ(true, has_header("Content-Type"));
+  EXPECT_EQ(true, has_header("Cache-Control"));
+  EXPECT_EQ(true, has_header("Last-Modified"));
+
+  // The non-safelisted header must be stripped.
+  EXPECT_EQ(false, has_header("X-Auth-Token"));
+}
+
 // Verifies that a generic MIME handler with `can_embed: false` is selected
 // for top-level navigations but bypassed for embedded loads.
 IN_PROC_BROWSER_TEST_F(GenericMimeHandlerBrowserTest,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/extensions/api/mime_handlers/generic_mime_handler_browsertest.cc b/chrome/browser/extensions/api/mime_handlers/generic_mime_handler_browsertest.cc
index 260fc43..2efb6e2 100644
--- a/chrome/browser/extensions/api/mime_handlers/generic_mime_handler_browsertest.cc
+++ b/chrome/browser/extensions/api/mime_handlers/generic_mime_handler_browsertest.cc
@@ -2,11 +2,15 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+#include <memory>
 #include <string>
 
 #include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/functional/bind.h"
 #include "base/path_service.h"
 #include "base/test/scoped_feature_list.h"
+#include "base/threading/thread_restrictions.h"
 #include "chrome/browser/content_settings/host_content_settings_map_factory.h"
 #include "chrome/browser/extensions/extension_apitest.h"
 #include "chrome/browser/plugins/plugin_utils.h"
@@ -36,6 +40,9 @@
 #include "extensions/common/manifest_handlers/mime_types_handler.h"
 #include "extensions/test/result_catcher.h"
 #include "net/dns/mock_host_resolver.h"
+#include "net/http/http_status_code.h"
+#include "net/test/embedded_test_server/http_request.h"
+#include "net/test/embedded_test_server/http_response.h"
 #include "services/network/public/cpp/permissions_policy/permissions_policy_features.h"
 #include "ui/base/page_transition_types.h"
 #include "url/url_constants.h"
@@ -47,6 +54,34 @@
 constexpr char kPdfMimeType[] = "application/pdf";
 constexpr char kTestExtensionDir[] = "generic_mime_handler";
 constexpr char kTestPdfPath[] = "/test.pdf";
+constexpr char kHeaderProbePdfPath[] = "/header-probe.pdf";
+constexpr char kAuthTokenHeaderName[] = "X-Auth-Token";
+
+// Serves `pdf_body` at kHeaderProbePdfPath with a mix of response headers:
+// CORS-safelisted ones (Content-Type via set_content_type, plus Cache-Control
+// and Last-Modified) that a generic handler must still see, and a
+// non-safelisted X-Auth-Token that must be filtered out. `pdf_body` is the
+// real test.pdf, so the response body is a valid document rather than an
+// ad-hoc literal (the test inspects only headers, so its exact bytes do not
+// matter, but a valid PDF keeps the fixture honest).
+std::unique_ptr<net::test_server::HttpResponse>
+HandlePdfWithExtraResponseHeaders(
+    const std::string& pdf_body,
+    const net::test_server::HttpRequest& request) {
+  // RegisterRequestHandler invokes every registered handler in turn; returning
+  // nullptr for any other path lets the static-file handlers serve it.
+  if (request.relative_url != kHeaderProbePdfPath) {
+    return nullptr;
+  }
+  auto response = std::make_unique<net::test_server::BasicHttpResponse>();
+  response->set_code(net::HTTP_OK);
+  response->set_content_type(kPdfMimeType);
+  response->AddCustomHeader("Cache-Control", "max-age=0");
+  response->AddCustomHeader("Last-Modified", "Wed, 21 Oct 2026 07:28:00 GMT");
+  response->AddCustomHeader(kAuthTokenHeaderName, "s3cr3t");
+  response->set_content(pdf_body);
+  return response;
+}
 
 }  // namespace
 
@@ -71,6 +106,17 @@
         test_data_dir.AppendASCII("pdf"));
     embedded_test_server()->ServeFilesFromDirectory(
         test_data_dir_.AppendASCII(kTestExtensionDir));
+    // Reuse the shared test PDF as the probe response body, so it is a real,
+    // valid document.
+    std::string pdf_body;
+    {
+      base::ScopedAllowBlockingForTesting allow_blocking;
+      ASSERT_TRUE(base::ReadFileToString(
+          test_data_dir_.AppendASCII(kTestExtensionDir).AppendASCII("test.pdf"),
+          &pdf_body));
+    }
+    embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
+        &HandlePdfWithExtraResponseHeaders, std::move(pdf_body)));
     ASSERT_TRUE(StartEmbeddedTestServer());
   }
 
@@ -150,6 +196,48 @@
   ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();
 }
 
+// A generic (third-party) MIME handler sees exactly the CORS-safelisted
+// response headers fetch() would expose cross-origin: the safelisted
+// Content-Type / Cache-Control / Last-Modified stay visible, while the custom
+// X-Auth-Token is filtered out. handler.js already asserts mimeType and the
+// %PDF- body on this same navigation, so this test covers only responseHeaders.
+IN_PROC_BROWSER_TEST_F(GenericMimeHandlerBrowserTest,
+                       GetStreamInfoFiltersNonSafelistedResponseHeaders) {
+  ASSERT_TRUE(LoadExtension(test_data_dir_.AppendASCII(kTestExtensionDir)));
+
+  ResultCatcher catcher;
+  ASSERT_TRUE(
+      NavigateToURL(GetActiveWebContents(),
+                    embedded_test_server()->GetURL(kHeaderProbePdfPath)));
+  ASSERT_TRUE(catcher.GetNextResult()) << catcher.message();
+
+  content::RenderFrameHost* extension_frame = FindMimeHandlerExtensionFrame();
+  ASSERT_TRUE(extension_frame);
+
+  // Whether getStreamInfo()'s responseHeaders contains `name`
+  // (case-insensitive).
+  auto has_header = [&](const char* name) {
+    return content::EvalJs(
+        extension_frame,
+        content::JsReplace(
+            "chrome.mimeHandler.getStreamInfo().then(info =>"
+            "  Object.keys(info.responseHeaders)"
+            "    .some(k => k.toLowerCase() === $1.toLowerCase()))",
+            name));
+  };
+
+  // Every CORS-safelisted response header the server sent must stay visible.
+  // These positive checks are also the vacuity guard: were responseHeaders
+  // ever empty or getStreamInfo() to fail, they would fail here instead of
+  // letting the X-Auth-Token check below pass against an empty object.
+  EXPECT_EQ(true, has_header("Content-Type"));
+  EXPECT_EQ(true, has_header("Cache-Control"));
+  EXPECT_EQ(true, has_header("Last-Modified"));
+
+  // The non-safelisted header must be stripped.
+  EXPECT_EQ(false, has_header("X-Auth-Token"));
+}
+
 // Verifies that a generic MIME handler with `can_embed: false` is selected
 // for top-level navigations but bypassed for embedded loads.
 IN_PROC_BROWSER_TEST_F(GenericMimeHandlerBrowserTest,
Loading diff…

Original Bug Report

reported by os...@paloaltonetworks.com

Cross-Origin User Information Disclosure by mime_types_handler

Security Bug

Important: Please do not change the component of this bug manually.

Please READ THIS FAQ before filing a bug: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md

Please see the following link for instructions on filing security bugs: https://www.chromium.org/Home/chromium-security/reporting-security-bugs

Reports may be eligible for reward payments under the Chrome VRP: https://g.co/chrome/vrp

NOTE: Security bugs are normally made public once a fix has been widely deployed.


VULNERABILITY DETAILS An extension that requests no permissions can set itself as the default handler for PDF documents. Since it requests no permissions - there will be no warning dialogue when the user installs the extension. When the user downloads a PDF inside any Chromium-based browser with the extension installed - the extension can access all of the HTTP Response Headers (except for Cookie based headers: set-cookie, set-cookie2, clear-site-data). I believe this is an unintended cross-origin user information disclosure, as the extension can access any other sensitive response header from any origin where the user gets a PDF with no warning at install-time nor at runtime.

VERSION Chrome Version: 152.0.7947.0 dev Operating System: MacOS 26.5 (25F71)

REPRODUCTION CASE

  1. Move manifest.json, viewer.html and viewer.js into a folder named extension and install this extension to a Chromium browser by installing an unpacked extension.
  2. Run server.py using Python 3
  3. Navigate in the browser to http://127.0.0.1:38180/victim.pdf
  4. You should now see an output that looks like this: { “sentinel”: “PDF-ID”, “originalUrl”: “http://127.0.0.1:38180/victim.pdf”, “mimeType”: “application/pdf”, “tabId”: 306537361, “embedded”: false, “bodyBytes”: 125, “bodyMagic”: “%PDF-”, “responseHeaders”: { “Content-Length”: “125”, “Content-Type”: “application/pdf”, “Date”: “Sun, 12 Jul 2026 15:01:14 GMT”, “Server”: “BaseHTTP/0.6 Python/3.14.6”, “X-Auth-Token”: “secret-would-be-here” } }

As you can see, X-Auth-Token appears and the extension was able to get it.

CREDIT INFORMATION Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited? Reporter credit: Oran Simhony from Palo Alto Networks

View on issue tracker