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 <[email protected]> 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 <[email protected]> Reviewed-by: Devlin Cronin <[email protected]> Reviewed-by: Mihai Sardarescu <[email protected]> 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