Medium watch CVSS 8.8 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Extensions
DescriptionInappropriate implementation in Extensions
ComponentExtensions
Sub-componentExtensions
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker40060076
Fix commitce0f402f288d (chromium/src) +260/-17
CWECWE-358
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedNDevTK
Reported2022-06-26 · bounty $1000 (per vendor advisory)
Fix landed2025-02-05 (public git)
Disclosed2025-04-01
Patch gap55 days — fix public in source before the advisory

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 during OnBeforeRequest/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

An extension could use a 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).
Key insight
The core mistake was that extension-initiated redirects into extension resources were not tracked, so the browser could not distinguish and block redirects landing on non-web-accessible resources; the fix records every extension redirect target in ExtensionNavigationRegistry so WAR enforcement can reject navigations to resources that were not declared web accessible.

Attack Path

  1. Load an extension with redirect capability An extension with webRequest/declarativeNetRequest redirect permissions is installed (the test uses both MV2 and MV3 manifests).
  2. Intercept a navigation and redirect it The extension intercepts a request during OnBeforeRequest/delta execution and redirects it toward an extension resource URL.
  3. Redirect resolves to a non-web-accessible resource Pre-fix, the redirect to a resource such as resource.html that is not listed in web_accessible_resources completed (net::OK), exposing an extension resource that should not be reachable via redirect (inferred).
  4. Recording enables enforcement With the fix, RecordThatNavigationWasInitiatedByExtension stores the redirect target under the navigation id in ExtensionNavigationRegistry, and (with kExtensionWARForRedirect enabled) the navigation to a non-WAR resource is blocked with net::ERR_BLOCKED_BY_CLIENT.

Impact Assessment

The weakness let an extension-initiated redirect reach extension resources that were not declared web accessible, an inappropriate-implementation / logic error in the Extensions WAR enforcement running in the browser process, with the precondition of an installed extension that can perform 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

FunctionChangeNotes
WebAccessibleResourcesBrowserRedirectTest
chrome/browser/extensions/web_accessible_resources_browsertest.cc
modified
WebAccessibleResourcesBrowserRedirectTest
chrome/browser/extensions/web_accessible_resources_browsertest.cc
modified
IN_PROC_BROWSER_TEST_F
chrome/browser/extensions/web_accessible_resources_browsertest.cc
modified
IN_PROC_BROWSER_TEST_P
chrome/browser/extensions/web_accessible_resources_browsertest.cc
modified

Files Changed

  • chrome/browser/extensions/web_accessible_resources_browsertest.cc
  • chrome/browser/profiles/profile_keyed_service_browsertest.cc
  • chrome/test/data/extensions/api_test/webrequest/test_redirects/manifest.json
  • extensions/browser/BUILD.gn
  • extensions/browser/api/web_request/extension_web_request_event_router.cc

Audit Directions

  • Redirects bypassing resource-access checks
    Audit 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 navigation
    Review 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 lifetime
    Examine ExtensionNavigationRegistry entries keyed by navigation_id for correct cleanup and for behavior when navigation_id is absent, since the recording is skipped in that case and enforcement must fail safe.
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();
 
Loading diff…

Regression Test / PoC

shipped with the fix
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"]
 }
Loading diff…

Original Bug Report

reported by [email protected]

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

https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/extensions/api/terminal/terminal_private_api.cc

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

View on issue tracker