CVE-2025-3069
Overview
Background
- Web Accessible Resources (WAR)
- Extension resources that a manifest explicitly lists as loadable from web pages; resources not listed are supposed to be inaccessible cross-origin.
- `webRequest` redirect
- An extension using the
webRequest/declarativeNetRequest machinery can intercept a network request in the browser process and redirect it to a new URL duringOnBeforeRequest/delta execution. - `ExtensionNavigationRegistry`
- A newly added
BrowserContext-keyed service that records, per navigation id, that a redirect was initiated by an extension (RecordExtensionRedirect), so later WAR-access decisions can know an extension caused the navigation. - `WebRequestInfo::navigation_id`
- The optional navigation identifier attached to a request, used as the key under which the extension-initiated redirect target is stored.
Root Cause Analysis
webRequest/declarativeNetRequest redirect to send a navigation to an extension resource, and the browser treated the redirect target’s access to extension resources without accounting for the fact that the redirect was extension-initiated, allowing a server/extension redirect to reach resources that are not listed as web accessible. The regression test encodes the intended post-fix behavior: with kExtensionWARForRedirect enabled, a server redirect to web_accessible_resource.html still succeeds (net::OK), but a redirect to the non-web-accessible resource.html now yields net::ERR_BLOCKED_BY_CLIENT instead of the old net::OK, i.e. the check is tightened to block redirects into non-WAR resources. This patch itself supplies the bookkeeping the enforcement relies on: it adds RecordThatNavigationWasInitiatedByExtension, which — when request->navigation_id is present — calls ExtensionNavigationRegistry::Get(browser_context)->RecordExtensionRedirect(navigation_id, new_location) to persist the extension-driven redirect target, and invokes it at the two redirect-producing points in the web-request event router (after a declarativeNetRequest redirect action returning net::OK, and after applying a blocked-request new_url). The registry is also registered as a profile-keyed service. The actual blocking decision that consumes this record and returns ERR_BLOCKED_BY_CLIENT is not contained in this diff (inferred).ExtensionNavigationRegistry so WAR enforcement can reject navigations to resources that were not declared web accessible.Attack Path
- Load an extension with redirect capability
An extension with
webRequest/declarativeNetRequest redirect permissions is installed (the test uses both MV2 and MV3 manifests). - Intercept a navigation and redirect it
The extension intercepts a request during
OnBeforeRequest/delta execution and redirects it toward an extension resource URL. - Redirect resolves to a non-web-accessible resource
Pre-fix, the redirect to a resource such as
resource.htmlthat is not listed inweb_accessible_resourcescompleted (net::OK), exposing an extension resource that should not be reachable via redirect(inferred). - Recording enables enforcement
With the fix,
RecordThatNavigationWasInitiatedByExtensionstores the redirect target under the navigation id inExtensionNavigationRegistry, and (withkExtensionWARForRedirectenabled) the navigation to a non-WAR resource is blocked withnet::ERR_BLOCKED_BY_CLIENT.
Impact Assessment
webRequest/DNR redirects. The advisory reports no in-the-wild exploitation; the concrete consequence of reaching a non-WAR resource (information disclosure or further access) is (inferred) and is not demonstrated by this diff, which supplies the redirect-recording groundwork rather than the terminal enforcement. The change is gated by the kExtensionWARForRedirect feature as exercised by the parameterized test.Changed Functions
| Function | Change | Notes |
|---|---|---|
WebAccessibleResourcesBrowserRedirectTestchrome/browser/extensions/web_accessible_resources_browsertest.cc |
modified | |
WebAccessibleResourcesBrowserRedirectTestchrome/browser/extensions/web_accessible_resources_browsertest.cc |
modified | |
IN_PROC_BROWSER_TEST_Fchrome/browser/extensions/web_accessible_resources_browsertest.cc |
modified | |
IN_PROC_BROWSER_TEST_Pchrome/browser/extensions/web_accessible_resources_browsertest.cc |
modified |
Files Changed
chrome/browser/extensions/web_accessible_resources_browsertest.ccchrome/browser/profiles/profile_keyed_service_browsertest.ccchrome/test/data/extensions/api_test/webrequest/test_redirects/manifest.jsonextensions/browser/BUILD.gnextensions/browser/api/web_request/extension_web_request_event_router.cc
Audit Directions
- Redirects bypassing resource-access checksAudit WAR and other origin/access checks to confirm they account for extension-initiated redirects (via navigation-id tracking), not just the initial request origin.
- Access decisions that ignore who caused a navigationReview places where a navigation’s initiator (extension vs. web) matters for access control but is not propagated through redirects, a gap this registry is designed to close.
- New per-navigation state lifetimeExamine
ExtensionNavigationRegistryentries keyed bynavigation_idfor correct cleanup and for behavior whennavigation_idis absent, since the recording is skipped in that case and enforcement must fail safe.
Patch
From ce0f402f288d3f0aacfdd843f57d8839613b70db Mon Sep 17 00:00:00 2001
From: Solomon Kinard <solomonkinard@chromium.org>
Date: Wed, 05 Feb 2025 06:54:39 -0800
Subject: [PATCH] Extensions: WAR: Prevent server redirect to non web accessible resources
Doc:
https://docs.google.com/document/d/1ALcxHF2m85pqxEtJVQ_shHqzlIBpr3747ceSDuw7E_w/edit?usp=sharing
Fixed: chromium:40060076
Change-Id: I0440d9556ccc793d1963e434c1bb4bd097745b2e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6160996
Commit-Queue: Solomon Kinard <solomonkinard@chromium.org>
Reviewed-by: Devlin Cronin <rdevlin.cronin@chromium.org>
Reviewed-by: Mihai Sardarescu <msarda@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1416132}
---
diff --git a/chrome/browser/extensions/web_accessible_resources_browsertest.cc b/chrome/browser/extensions/web_accessible_resources_browsertest.cc
index 6d88f39..8e7448a 100644
--- a/chrome/browser/extensions/web_accessible_resources_browsertest.cc
+++ b/chrome/browser/extensions/web_accessible_resources_browsertest.cc
@@ -461,9 +461,18 @@
// TODO(crbug.com/390687767): Port to desktop Android. Currently the redirect
// doesn't happen.
class WebAccessibleResourcesBrowserRedirectTest
- : public WebAccessibleResourcesBrowserTest {
+ : public WebAccessibleResourcesBrowserTest,
+ public testing::WithParamInterface<bool> {
+ public:
+ WebAccessibleResourcesBrowserRedirectTest() {
+ feature_list_.InitWithFeatureState(
+ extensions_features::kExtensionWARForRedirect, GetParam());
+ }
+
protected:
- void TestBrowserRedirect(const char* kManifest, const char* kHistogramName) {
+ void TestBrowserRedirect(const char* kManifest,
+ const char* kHistogramName,
+ bool is_war_for_redirect_enabled) {
// Load extension.
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
@@ -496,10 +505,12 @@
// Test cases.
server_redirect(net::OK, "web_accessible_resource.html", true);
- server_redirect(net::OK, "resource.html", false);
+ server_redirect(
+ is_war_for_redirect_enabled ? net::ERR_BLOCKED_BY_CLIENT : net::OK,
+ "resource.html", false);
}
- void TestBrowserRedirectMV2() {
+ void TestBrowserRedirectMV2(bool is_war_for_redirect_enabled) {
TestBrowserRedirect(
R"({
"name": "Test browser redirect",
@@ -507,10 +518,10 @@
"manifest_version": 2,
"web_accessible_resources": ["web_accessible_resource.html"]
})",
- "Extensions.WAR.XOriginWebAccessible.MV2");
+ "Extensions.WAR.XOriginWebAccessible.MV2", is_war_for_redirect_enabled);
}
- void TestBrowserRedirectMV3() {
+ void TestBrowserRedirectMV3(bool is_war_for_redirect_enabled) {
TestBrowserRedirect(
R"({
"name": "Redirect Test",
@@ -523,15 +534,24 @@
}
]
})",
- "Extensions.WAR.XOriginWebAccessible.MV3");
+ "Extensions.WAR.XOriginWebAccessible.MV3", is_war_for_redirect_enabled);
}
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
};
+INSTANTIATE_TEST_SUITE_P(All,
+ WebAccessibleResourcesBrowserRedirectTest,
+ testing::Bool());
+
// Test server redirect to a web accessible or extension resource.
-IN_PROC_BROWSER_TEST_F(WebAccessibleResourcesBrowserRedirectTest, Manifests) {
- TestBrowserRedirectMV2();
- TestBrowserRedirectMV3();
+IN_PROC_BROWSER_TEST_P(WebAccessibleResourcesBrowserRedirectTest, Manifests) {
+ bool is_war_for_redirect_enabled = GetParam();
+ TestBrowserRedirectMV2(is_war_for_redirect_enabled);
+ TestBrowserRedirectMV3(is_war_for_redirect_enabled);
}
+
#endif // !BUILDFLAG(IS_ANDROID)
} // namespace
diff --git a/chrome/browser/profiles/profile_keyed_service_browsertest.cc b/chrome/browser/profiles/profile_keyed_service_browsertest.cc
index 68a1b03..67697d9 100644
--- a/chrome/browser/profiles/profile_keyed_service_browsertest.cc
+++ b/chrome/browser/profiles/profile_keyed_service_browsertest.cc
@@ -376,6 +376,7 @@
"ExtensionInstallEventRouter",
#endif // BUILDFLAG(ENTERPRISE_CONTENT_ANALYSIS)
"ChromeEnterpriseRealTimeUrlLookupService",
+ "ExtensionNavigationRegistry",
"ExtensionSystem",
"ExtensionURLLoaderFactory::BrowserContextShutdownNotifierFactory",
"FederatedIdentityPermissionContext",
@@ -628,6 +629,7 @@
"HeavyAdService",
#if BUILDFLAG(ENABLE_EXTENSIONS)
"HidConnectionResourceManager",
+ "ExtensionNavigationRegistry",
#endif
"HidDeviceManager",
"HistoryAPI",
diff --git a/chrome/test/data/extensions/api_test/webrequest/test_redirects/manifest.json b/chrome/test/data/extensions/api_test/webrequest/test_redirects/manifest.json
index 1a8c500f..e93bba06 100644
--- a/chrome/test/data/extensions/api_test/webrequest/test_redirects/manifest.json
+++ b/chrome/test/data/extensions/api_test/webrequest/test_redirects/manifest.json
@@ -8,5 +8,6 @@
"background": {
"scripts": ["test_redirects.js"],
"persistent": true
- }
+ },
+ "web_accessible_resources": ["simpleLoad/a.html"]
}
diff --git a/extensions/browser/BUILD.gn b/extensions/browser/BUILD.gn
index a70c529..9551a25 100644
--- a/extensions/browser/BUILD.gn
+++ b/extensions/browser/BUILD.gn
@@ -312,6 +312,8 @@
"extension_icon_manager.h",
"extension_icon_placeholder.cc",
"extension_icon_placeholder.h",
+ "extension_navigation_registry.cc",
+ "extension_navigation_registry.h",
"extension_navigation_throttle.cc",
"extension_navigation_throttle.h",
"extension_navigation_ui_data.cc",
diff --git a/extensions/browser/api/web_request/extension_web_request_event_router.cc b/extensions/browser/api/web_request/extension_web_request_event_router.cc
index ed36da5..e98ff00 100644
--- a/extensions/browser/api/web_request/extension_web_request_event_router.cc
+++ b/extensions/browser/api/web_request/extension_web_request_event_router.cc
@@ -40,6 +40,7 @@
#include "extensions/browser/api/web_request/web_request_time_tracker.h"
#include "extensions/browser/api_activity_monitor.h"
#include "extensions/browser/event_router.h"
+#include "extensions/browser/extension_navigation_registry.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/browser/process_map.h"
@@ -408,6 +409,23 @@
return dynamic_url.value_or(redirect_url);
}
+// Write that extension caused redirect OnBeforeRequest, ExecuteDeltas.
+void RecordThatNavigationWasInitiatedByExtension(
+ const WebRequestInfo* request,
+ content::BrowserContext* browser_context,
+ GURL* new_url) {
+ GURL new_location = new_url ? *new_url : GURL();
+
+ // Now that the event type has been signaled, record that webRequest has
+ // intercepted this redirect.
+ if (request->navigation_id.has_value()) {
+ // Store the target url.
+ // TODO(crbug.com/40060076): Record the extension id that caused the action.
+ ExtensionNavigationRegistry::Get(browser_context)
+ ->RecordExtensionRedirect(request->navigation_id.value(), new_location);
+ }
+}
+
using CallbacksForPageLoad = std::list<base::OnceClosure>;
// TODO(crbug.com/40264286): We need to investigate why this is a global
@@ -1006,7 +1024,8 @@
browser_context, action.extension_id, request->url,
action.redirect_url.value());
}
-
+ RecordThatNavigationWasInitiatedByExtension(request, browser_context,
+ new_url);
return net::OK;
case DNRRequestAction::Type::MODIFY_HEADERS:
// Unlike other actions, allow web request extensions to intercept
@@ -2462,6 +2481,9 @@
OnDNRActionMatched(browser_context, *request, *action);
}
+ RecordThatNavigationWasInitiatedByExtension(request, browser_context,
+ blocked_request.new_url);
+
const bool redirected =
blocked_request.new_url && !blocked_request.new_url->is_empty();
Regression Test / PoC
diff --git a/chrome/browser/extensions/web_accessible_resources_browsertest.cc b/chrome/browser/extensions/web_accessible_resources_browsertest.cc
index 6d88f39..8e7448a 100644
--- a/chrome/browser/extensions/web_accessible_resources_browsertest.cc
+++ b/chrome/browser/extensions/web_accessible_resources_browsertest.cc
@@ -461,9 +461,18 @@
// TODO(crbug.com/390687767): Port to desktop Android. Currently the redirect
// doesn't happen.
class WebAccessibleResourcesBrowserRedirectTest
- : public WebAccessibleResourcesBrowserTest {
+ : public WebAccessibleResourcesBrowserTest,
+ public testing::WithParamInterface<bool> {
+ public:
+ WebAccessibleResourcesBrowserRedirectTest() {
+ feature_list_.InitWithFeatureState(
+ extensions_features::kExtensionWARForRedirect, GetParam());
+ }
+
protected:
- void TestBrowserRedirect(const char* kManifest, const char* kHistogramName) {
+ void TestBrowserRedirect(const char* kManifest,
+ const char* kHistogramName,
+ bool is_war_for_redirect_enabled) {
// Load extension.
TestExtensionDir test_dir;
test_dir.WriteManifest(kManifest);
@@ -496,10 +505,12 @@
// Test cases.
server_redirect(net::OK, "web_accessible_resource.html", true);
- server_redirect(net::OK, "resource.html", false);
+ server_redirect(
+ is_war_for_redirect_enabled ? net::ERR_BLOCKED_BY_CLIENT : net::OK,
+ "resource.html", false);
}
- void TestBrowserRedirectMV2() {
+ void TestBrowserRedirectMV2(bool is_war_for_redirect_enabled) {
TestBrowserRedirect(
R"({
"name": "Test browser redirect",
@@ -507,10 +518,10 @@
"manifest_version": 2,
"web_accessible_resources": ["web_accessible_resource.html"]
})",
- "Extensions.WAR.XOriginWebAccessible.MV2");
+ "Extensions.WAR.XOriginWebAccessible.MV2", is_war_for_redirect_enabled);
}
- void TestBrowserRedirectMV3() {
+ void TestBrowserRedirectMV3(bool is_war_for_redirect_enabled) {
TestBrowserRedirect(
R"({
"name": "Redirect Test",
@@ -523,15 +534,24 @@
}
]
})",
- "Extensions.WAR.XOriginWebAccessible.MV3");
+ "Extensions.WAR.XOriginWebAccessible.MV3", is_war_for_redirect_enabled);
}
+
+ private:
+ base::test::ScopedFeatureList feature_list_;
};
+INSTANTIATE_TEST_SUITE_P(All,
+ WebAccessibleResourcesBrowserRedirectTest,
+ testing::Bool());
+
// Test server redirect to a web accessible or extension resource.
-IN_PROC_BROWSER_TEST_F(WebAccessibleResourcesBrowserRedirectTest, Manifests) {
- TestBrowserRedirectMV2();
- TestBrowserRedirectMV3();
+IN_PROC_BROWSER_TEST_P(WebAccessibleResourcesBrowserRedirectTest, Manifests) {
+ bool is_war_for_redirect_enabled = GetParam();
+ TestBrowserRedirectMV2(is_war_for_redirect_enabled);
+ TestBrowserRedirectMV3(is_war_for_redirect_enabled);
}
+
#endif // !BUILDFLAG(IS_ANDROID)
} // namespace
diff --git a/chrome/browser/profiles/profile_keyed_service_browsertest.cc b/chrome/browser/profiles/profile_keyed_service_browsertest.cc
index 68a1b03..67697d9 100644
--- a/chrome/browser/profiles/profile_keyed_service_browsertest.cc
+++ b/chrome/browser/profiles/profile_keyed_service_browsertest.cc
@@ -376,6 +376,7 @@
"ExtensionInstallEventRouter",
#endif // BUILDFLAG(ENTERPRISE_CONTENT_ANALYSIS)
"ChromeEnterpriseRealTimeUrlLookupService",
+ "ExtensionNavigationRegistry",
"ExtensionSystem",
"ExtensionURLLoaderFactory::BrowserContextShutdownNotifierFactory",
"FederatedIdentityPermissionContext",
@@ -628,6 +629,7 @@
"HeavyAdService",
#if BUILDFLAG(ENABLE_EXTENSIONS)
"HidConnectionResourceManager",
+ "ExtensionNavigationRegistry",
#endif
"HidDeviceManager",
"HistoryAPI",
diff --git a/chrome/test/data/extensions/api_test/webrequest/test_redirects/manifest.json b/chrome/test/data/extensions/api_test/webrequest/test_redirects/manifest.json
index 1a8c500f..e93bba06 100644
--- a/chrome/test/data/extensions/api_test/webrequest/test_redirects/manifest.json
+++ b/chrome/test/data/extensions/api_test/webrequest/test_redirects/manifest.json
@@ -8,5 +8,6 @@
"background": {
"scripts": ["test_redirects.js"],
"persistent": true
- }
+ },
+ "web_accessible_resources": ["simpleLoad/a.html"]
}
Original Bug Report
Prevent server redirect to non web accessible resource
Steps to reproduce the problem:
chrome.window.create({url: ‘chrome-extension://nkoccljplnhpfnfiajclkommnmllphnl/html/crosh.html?command=vmshell&args[]=’})
Problem Description:
The URL parameters args[] and command on chrome-extension://nkoccljplnhpfnfiajclkommnmllphnl/html/crosh.html
Are used in chrome.terminalPrivate.openTerminalProcess or chrome.terminalPrivate.openVmshellProcess
This allows an extension to trigger crosh and run vmshell with arguments.
Interestingly if you type a url in the omnibox a https:// resource is allowed to redirect to crosh. its treated as sec-fetch-site none not sure if its meant to be cross-origin.
Unrelated but I noticed ChromeOS leaks if files and folders exist to guest users file:///etc/passwd and file:///etc/ says ERROR_ACCESS_DENIED while file:///etc/foo says ERROR_FILE_NOT_FOUND
I understand if this is a non-issue.
Additional Comments:
**Chrome version: ** 103.0.0.0 **Channel: ** Not sure
OS: Windows