Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in API
DescriptionUse after free in API
ComponentAPI
Sub-componentExtensions
Bug ClassUAF
Tracker513452101
Fix commit07ea40aaa31a (chromium/src) +176/-31
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
if
extensions/browser/api/declarative_net_request/ruleset_matcher_base.cc
modified
if
extensions/browser/api/web_request/extension_web_request_event_router.cc
modified
IN_PROC_BROWSER_TEST_P
chrome/browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.cc
modified
DeclarativeNetRequestBackForwardCacheBrowserTest
chrome/browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.cc
modified
TEST
chrome/browser/extensions/api/web_request/web_request_api_unittest.cc
modified

Files Changed

  • extensions/browser/api/declarative_net_request/flat_ruleset_indexer.cc
  • extensions/browser/api/declarative_net_request/ruleset_matcher_unittest.cc
  • extensions/browser/api/web_request/web_request_api_helpers.cc
  • chrome/browser/extensions/api/web_request/web_request_api_unittest.cc
  • extensions/browser/api/web_request/extension_web_request_event_router.cc
  • chrome/browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.cc
  • extensions/browser/api/web_request/web_request_api_helpers.h
  • extensions/browser/api/declarative_net_request/ruleset_matcher_base.cc
From 07ea40aaa31a3395bd83775ea1c49ff770d5f005 Mon Sep 17 00:00:00 2001
From: Kelvin Jiang <[email protected]>
Date: Wed, 23 Sep 2026 23:12:10 -0700
Subject: [PATCH] [Extensions] Ignore redirect actions for WebTransport handshakes

WebTransport handshakes cannot be redirected by the request stack
(WebTransportHandshakeProxy ignores any redirect URL), so suppress
redirect actions for them in both Declarative Net Request
(RulesetMatcherBase::CreateRedirectAction) and webRequest
(MergeRedirectUrlOfResponsesHelper), matching how WebSocket handshakes
are already handled.

Without this, a matching redirect rule/response is recorded as applied
(e.g. in action count badges and getMatchedRules) and short-circuits
OnBeforeRequest without taking effect on the WebTransport connection.

Also adds unit and browser test coverage for both WebSocket and
WebTransport cases.

Fixed: 513452101
Change-Id: I1b4593717146dba051f9864ff7f356fdc1a80e64
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8408232
Reviewed-by: Devlin Cronin <[email protected]>
Commit-Queue: Kelvin Jiang <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1704050}
---
diff --git a/extensions/browser/api/declarative_net_request/flat_ruleset_indexer.cc b/extensions/browser/api/declarative_net_request/flat_ruleset_indexer.cc
index 5ece268..6fe3073 100644
--- a/extensions/browser/api/declarative_net_request/flat_ruleset_indexer.cc
+++ b/extensions/browser/api/declarative_net_request/flat_ruleset_indexer.cc
@@ -348,6 +348,11 @@
   auto embedder_conditions_offset =
       BuildEmbedderConditionsOffset(&builder_, indexed_rule);
 
+  // TODO(crbug.com/563832248): Redirecting WebSocket or WebTransport handshakes
+  // is not supported, so `ElementType_WEBSOCKET` and `ElementType_WEBTRANSPORT`
+  // should be excluded from `element_types` for redirect rules here. This will
+  // allow lower-priority rules in the same ruleset (e.g., block rules) to
+  // match.
   FlatOffset<flat_rule::UrlRule> offset = flat_rule::CreateUrlRule(
       builder_, indexed_rule.options, indexed_rule.element_types,
       indexed_rule.request_methods, indexed_rule.activation_types,
diff --git a/extensions/browser/api/declarative_net_request/ruleset_matcher_base.cc b/extensions/browser/api/declarative_net_request/ruleset_matcher_base.cc
index e11759a..579feb4f 100644
--- a/extensions/browser/api/declarative_net_request/ruleset_matcher_base.cc
+++ b/extensions/browser/api/declarative_net_request/ruleset_matcher_base.cc
@@ -344,7 +344,8 @@
     const RequestParams& params,
     const url_pattern_index::flat::UrlRule& rule) const {
   if (!IsUpgradeableUrl(*params.url)) {
-    // TODO(crbug.com/40111509): this results in counterintuitive behavior.
+    // TODO(crbug.com/40111509, crbug.com/563832248): this results in
+    // counterintuitive behavior.
     return std::nullopt;
   }
   RequestAction upgrade_action =
@@ -387,9 +388,10 @@
     const RequestParams& params,
     const url_pattern_index::flat::UrlRule& rule,
     GURL redirect_url) const {
-  // Redirecting WebSocket handshake request is prohibited.
+  // Redirecting WebSocket and WebTransport handshake requests is prohibited.
   // TODO(crbug.com/40111509): this results in counterintuitive behavior.
-  if (params.element_type == flat_rule::ElementType_WEBSOCKET) {
+  if (params.element_type == flat_rule::ElementType_WEBSOCKET ||
+      params.element_type == flat_rule::ElementType_WEBTRANSPORT) {
     return std::nullopt;
   }
 
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 c11c005e..7e2fc38 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
@@ -3262,7 +3262,7 @@
   if (blocked_request.event == EventTypes::kOnBeforeRequest) {
     CHECK(!blocked_request.callback.is_null());
     helpers::MergeOnBeforeRequestResponses(
-        request->url, blocked_request.response_deltas, blocked_request.new_url,
+        *request, blocked_request.response_deltas, blocked_request.new_url,
         &extension_id, &ignored_actions);
   } else if (blocked_request.event == EventTypes::kOnBeforeSendHeaders) {
     CHECK(!blocked_request.before_send_headers_callback.is_null());
diff --git a/extensions/browser/api/web_request/web_request_api_helpers.cc b/extensions/browser/api/web_request/web_request_api_helpers.cc
index 7b62b564..05ddd75 100644
--- a/extensions/browser/api/web_request/web_request_api_helpers.cc
+++ b/extensions/browser/api/web_request/web_request_api_helpers.cc
@@ -935,14 +935,16 @@
 // a higher precedence operation that redirects.
 // Returns whether a redirect occurred.
 static bool MergeRedirectUrlOfResponsesHelper(
-    const GURL& url,
+    const extensions::WebRequestInfo& request,
     const EventResponseDeltas& deltas,
     GURL* new_url,
     std::optional<extensions::ExtensionId>* extension_id,
     IgnoredActions* ignored_actions,
     bool consider_only_cancel_scheme_urls) {
-  // Redirecting WebSocket handshake request is prohibited.
-  if (url.SchemeIsWSOrWSS()) {
+  // Redirecting WebSocket or WebTransport handshake request is prohibited.
+  if (request.url.SchemeIsWSOrWSS() ||
+      request.web_request_type ==
+          extensions::WebRequestResourceType::WEB_TRANSPORT) {
     return false;
   }
 
@@ -971,14 +973,14 @@
 }
 
 void MergeRedirectUrlOfResponses(
-    const GURL& url,
+    const extensions::WebRequestInfo& request,
     const EventResponseDeltas& deltas,
     GURL* new_url,
     std::optional<extensions::ExtensionId>* extension_id,
     IgnoredActions* ignored_actions) {
   // First handle only redirects to data:// URLs and about:blank. These are a
   // special case as they represent a way of cancelling a request.
-  if (MergeRedirectUrlOfResponsesHelper(url, deltas, new_url, extension_id,
+  if (MergeRedirectUrlOfResponsesHelper(request, deltas, new_url, extension_id,
                                         ignored_actions, true)) {
     // If any extension cancelled a request by redirecting to a data:// URL or
     // about:blank, we don't consider the other redirects.
@@ -986,17 +988,17 @@
   }
 
   // Handle all other redirects.
-  MergeRedirectUrlOfResponsesHelper(url, deltas, new_url, extension_id,
+  MergeRedirectUrlOfResponsesHelper(request, deltas, new_url, extension_id,
                                     ignored_actions, false);
 }
 
 void MergeOnBeforeRequestResponses(
-    const GURL& url,
+    const extensions::WebRequestInfo& request,
     const EventResponseDeltas& deltas,
     GURL* new_url,
     std::optional<extensions::ExtensionId>* extension_id,
     IgnoredActions* ignored_actions) {
-  MergeRedirectUrlOfResponses(url, deltas, new_url, extension_id,
+  MergeRedirectUrlOfResponses(request, deltas, new_url, extension_id,
                               ignored_actions);
 }
 
@@ -1709,7 +1711,7 @@
 
   GURL new_url;
   std::optional<extensions::ExtensionId> extension_id;
-  MergeRedirectUrlOfResponses(request.url, deltas, &new_url, &extension_id,
+  MergeRedirectUrlOfResponses(request, deltas, &new_url, &extension_id,
                               ignored_actions);
   if (new_url.is_valid()) {
     // Only create a copy if we really want to modify the response headers.
diff --git a/extensions/browser/api/web_request/web_request_api_helpers.h b/extensions/browser/api/web_request/web_request_api_helpers.h
index 6dc5e0d6..b131ef17 100644
--- a/extensions/browser/api/web_request/web_request_api_helpers.h
+++ b/extensions/browser/api/web_request/web_request_api_helpers.h
@@ -424,7 +424,7 @@
 // precedence. Extensions that did not command to redirect the request are
 // ignored in this logic.
 void MergeRedirectUrlOfResponses(
-    const GURL& url,
+    const extensions::WebRequestInfo& request,
     const EventResponseDeltas& deltas,
     GURL* new_url,
     std::optional<extensions::ExtensionId>* extension_id,
@@ -433,7 +433,7 @@
 // precedence. Extensions that did not command to redirect the request are
 // ignored in this logic.
 void MergeOnBeforeRequestResponses(
-    const GURL& url,
+    const extensions::WebRequestInfo& request,
     const EventResponseDeltas& deltas,
     GURL* new_url,
     std::optional<extensions::ExtensionId>* extension_id,
diff --git a/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.cc b/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.cc
index 79518ab..0a156ece 100644
--- a/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.cc
+++ b/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.cc
@@ -7199,6 +7199,79 @@
                                 webtransport_server_.server_address().port())));
 }
 
+// Tests that redirect rules are not applied to WebTransport handshakes and
+// no false-positive matched rules are recorded.
+IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestWebTransportTest, RedirectIgnored) {
+  set_config_flags(ConfigFlag::kConfig_HasBackgroundScript |
+                   ConfigFlag::kConfig_HasFeedbackPermission);
+
+  ASSERT_TRUE(https_server()->Start());
+
+  // Rule 1 is a redirect rule for echo1. Since redirect is unsupported for
+  // WebTransport, this rule should be ignored, the connection should succeed,
+  // and the rule should not be counted as matched.
+  TestRule rule1 = CreateGenericRule(1);
+  rule1.condition->url_filter = "echo1_redirect";
+  rule1.priority = kMinValidPriority;
+  rule1.action->type = "redirect";
+  rule1.action->redirect.emplace();
+  rule1.action->redirect->url = "https://other.com/";
+
+  // Rule 2 is a block rule for echo2. This should successfully block the
+  // WebTransport handshake and be recorded in matched rules.
+  TestRule rule2 = CreateGenericRule(2);
+  rule2.condition->url_filter = "echo2_block";
+  rule2.priority = kMinValidPriority;
+  rule2.action->type = "block";
+
+  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules(
+      {rule1, rule2}, "test_extension", {URLPattern::kAllUrlsPattern}));
+
+  NavigateToURL(https_server()->GetURL("/echo"));
+
+  static constexpr char kOpenWebTransportScript[] = R"((
+    async () =>
+    {
+      const testCases = ["echo1_redirect", "echo2_block"];
+
+      let blockedTestCases = [];
+
+      await Promise.allSettled(
+        testCases.map(testCase =>
+          new Promise(async (resolve) =>
+          {
+            try {
+              const transport = new WebTransport(
+                `https://localhost:%d/${testCase}`);
+              // `transport.ready` only resolves if the handshake with the local
+              // server succeeds. Any failure (whether blocked by DNR or
+              // redirected to the unreachable "https://other.com/") rejects and
+              // enters `catch`.
+              await transport.ready;
+              transport.close();
+            } catch (e) {
+              blockedTestCases.push(testCase);
+            }
+            resolve();
+          })
+        )
+      );
+      return blockedTestCases.sort().join();
+    }
+  )())";
+
+  content::RenderFrameHost* main_frame = GetPrimaryMainFrame();
+  EXPECT_EQ("echo2_block",
+            content::EvalJs(main_frame,
+                            base::StringPrintf(
+                                kOpenWebTransportScript,
+                                webtransport_server_.server_address().port())));
+
+  // Verify that only the block rule (rule 2) was recorded as matched. The
+  // redirect rule (rule 1) should not have matched.
+  EXPECT_EQ("2", GetRuleIdsMatched(last_loaded_extension_id(), base::Time()));
+}
+
 // TODO(crbug.com/393191910): Port to desktop Android. These tests fail with
 // no logging and no stack.
 class DeclarativeNetRequestBackForwardCacheBrowserTest
diff --git a/chrome/browser/extensions/api/web_request/web_request_api_unittest.cc b/chrome/browser/extensions/api/web_request/web_request_api_unittest.cc
index e77f9130..c6c4849 100644
--- a/chrome/browser/extensions/api/web_request/web_request_api_unittest.cc
+++ b/chrome/browser/extensions/api/web_request/web_request_api_unittest.cc
@@ -847,12 +847,16 @@
   GURL effective_new_url;
   std::optional<ExtensionId> extension_id;
 
+  WebRequestInfoInitParams info_params;
+  info_params.url = GURL(kExampleUrl);
+  WebRequestInfo request_info(std::move(info_params));
+
   // No redirect
   {
     EventResponseDelta d0("extid0", base::Time::FromInternalValue(0));
     deltas.push_back(std::move(d0));
   }
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_TRUE(effective_new_url.is_empty());
   EXPECT_FALSE(extension_id.has_value());
@@ -865,7 +869,7 @@
     deltas.push_back(std::move(d1));
   }
   deltas.sort(&InDecreasingExtensionInstallationTimeOrder);
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_1, effective_new_url);
   EXPECT_EQ("extid1", extension_id.value());
@@ -880,7 +884,7 @@
   }
   deltas.sort(&InDecreasingExtensionInstallationTimeOrder);
   ignored_actions.clear();
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_1, effective_new_url);
   EXPECT_EQ("extid1", extension_id.value());
@@ -897,7 +901,7 @@
   }
   deltas.sort(&InDecreasingExtensionInstallationTimeOrder);
   ignored_actions.clear();
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_3, effective_new_url);
   EXPECT_EQ("extid3", extension_id.value());
@@ -915,7 +919,7 @@
   }
   deltas.sort(&InDecreasingExtensionInstallationTimeOrder);
   ignored_actions.clear();
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_3, effective_new_url);
   EXPECT_EQ("extid3", extension_id.value());
@@ -934,6 +938,10 @@
   GURL effective_new_url;
   std::optional<ExtensionId> extension_id;
 
+  WebRequestInfoInitParams info_params;
+  info_params.url = GURL(kExampleUrl);
+  WebRequestInfo request_info(std::move(info_params));
+
   // Single redirect.
   GURL new_url_0("http://foo.com");
   {
@@ -941,7 +949,7 @@
     d0.new_url = GURL(new_url_0);
     deltas.push_back(std::move(d0));
   }
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_0, effective_new_url);
   EXPECT_EQ("extid0", extension_id.value());
@@ -956,7 +964,7 @@
   }
   deltas.sort(&InDecreasingExtensionInstallationTimeOrder);
   ignored_actions.clear();
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_1, effective_new_url);
   EXPECT_EQ("extid1", extension_id.value());
@@ -973,7 +981,7 @@
   deltas.sort(&InDecreasingExtensionInstallationTimeOrder);
   ignored_actions.clear();
 
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_2, effective_new_url);
   EXPECT_EQ("extid2", extension_id.value());
@@ -989,7 +997,7 @@
   }
   deltas.sort(&InDecreasingExtensionInstallationTimeOrder);
   ignored_actions.clear();
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_1, effective_new_url);
   EXPECT_EQ("extid2", extension_id.value());
@@ -1006,6 +1014,10 @@
   GURL effective_new_url;
   std::optional<ExtensionId> extension_id;
 
+  WebRequestInfoInitParams info_params;
+  info_params.url = GURL(kExampleUrl);
+  WebRequestInfo request_info(std::move(info_params));
+
   // Single redirect.
   GURL new_url_0("http://foo.com");
   {
@@ -1013,7 +1025,7 @@
     d0.new_url = GURL(new_url_0);
     deltas.push_back(std::move(d0));
   }
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_0, effective_new_url);
   EXPECT_EQ("extid0", extension_id.value());
@@ -1028,14 +1040,14 @@
   }
   deltas.sort(&InDecreasingExtensionInstallationTimeOrder);
   ignored_actions.clear();
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_1, effective_new_url);
   EXPECT_EQ("extid1", extension_id.value());
   EXPECT_TRUE(ignored_actions.empty());
 }
 
-// This tests that WebSocket requests can not be redirected.
+// This tests that WebSocket and WebTransport requests can not be redirected.
 TEST(ExtensionWebRequestHelpersTest, TestMergeOnBeforeRequestResponses4) {
   EventResponseDeltas deltas;
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.cc b/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.cc
index 79518ab..0a156ece 100644
--- a/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.cc
+++ b/chrome/browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.cc
@@ -7199,6 +7199,79 @@
                                 webtransport_server_.server_address().port())));
 }
 
+// Tests that redirect rules are not applied to WebTransport handshakes and
+// no false-positive matched rules are recorded.
+IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestWebTransportTest, RedirectIgnored) {
+  set_config_flags(ConfigFlag::kConfig_HasBackgroundScript |
+                   ConfigFlag::kConfig_HasFeedbackPermission);
+
+  ASSERT_TRUE(https_server()->Start());
+
+  // Rule 1 is a redirect rule for echo1. Since redirect is unsupported for
+  // WebTransport, this rule should be ignored, the connection should succeed,
+  // and the rule should not be counted as matched.
+  TestRule rule1 = CreateGenericRule(1);
+  rule1.condition->url_filter = "echo1_redirect";
+  rule1.priority = kMinValidPriority;
+  rule1.action->type = "redirect";
+  rule1.action->redirect.emplace();
+  rule1.action->redirect->url = "https://other.com/";
+
+  // Rule 2 is a block rule for echo2. This should successfully block the
+  // WebTransport handshake and be recorded in matched rules.
+  TestRule rule2 = CreateGenericRule(2);
+  rule2.condition->url_filter = "echo2_block";
+  rule2.priority = kMinValidPriority;
+  rule2.action->type = "block";
+
+  ASSERT_NO_FATAL_FAILURE(LoadExtensionWithRules(
+      {rule1, rule2}, "test_extension", {URLPattern::kAllUrlsPattern}));
+
+  NavigateToURL(https_server()->GetURL("/echo"));
+
+  static constexpr char kOpenWebTransportScript[] = R"((
+    async () =>
+    {
+      const testCases = ["echo1_redirect", "echo2_block"];
+
+      let blockedTestCases = [];
+
+      await Promise.allSettled(
+        testCases.map(testCase =>
+          new Promise(async (resolve) =>
+          {
+            try {
+              const transport = new WebTransport(
+                `https://localhost:%d/${testCase}`);
+              // `transport.ready` only resolves if the handshake with the local
+              // server succeeds. Any failure (whether blocked by DNR or
+              // redirected to the unreachable "https://other.com/") rejects and
+              // enters `catch`.
+              await transport.ready;
+              transport.close();
+            } catch (e) {
+              blockedTestCases.push(testCase);
+            }
+            resolve();
+          })
+        )
+      );
+      return blockedTestCases.sort().join();
+    }
+  )())";
+
+  content::RenderFrameHost* main_frame = GetPrimaryMainFrame();
+  EXPECT_EQ("echo2_block",
+            content::EvalJs(main_frame,
+                            base::StringPrintf(
+                                kOpenWebTransportScript,
+                                webtransport_server_.server_address().port())));
+
+  // Verify that only the block rule (rule 2) was recorded as matched. The
+  // redirect rule (rule 1) should not have matched.
+  EXPECT_EQ("2", GetRuleIdsMatched(last_loaded_extension_id(), base::Time()));
+}
+
 // TODO(crbug.com/393191910): Port to desktop Android. These tests fail with
 // no logging and no stack.
 class DeclarativeNetRequestBackForwardCacheBrowserTest
diff --git a/chrome/browser/extensions/api/web_request/web_request_api_unittest.cc b/chrome/browser/extensions/api/web_request/web_request_api_unittest.cc
index e77f9130..c6c4849 100644
--- a/chrome/browser/extensions/api/web_request/web_request_api_unittest.cc
+++ b/chrome/browser/extensions/api/web_request/web_request_api_unittest.cc
@@ -847,12 +847,16 @@
   GURL effective_new_url;
   std::optional<ExtensionId> extension_id;
 
+  WebRequestInfoInitParams info_params;
+  info_params.url = GURL(kExampleUrl);
+  WebRequestInfo request_info(std::move(info_params));
+
   // No redirect
   {
     EventResponseDelta d0("extid0", base::Time::FromInternalValue(0));
     deltas.push_back(std::move(d0));
   }
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_TRUE(effective_new_url.is_empty());
   EXPECT_FALSE(extension_id.has_value());
@@ -865,7 +869,7 @@
     deltas.push_back(std::move(d1));
   }
   deltas.sort(&InDecreasingExtensionInstallationTimeOrder);
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_1, effective_new_url);
   EXPECT_EQ("extid1", extension_id.value());
@@ -880,7 +884,7 @@
   }
   deltas.sort(&InDecreasingExtensionInstallationTimeOrder);
   ignored_actions.clear();
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_1, effective_new_url);
   EXPECT_EQ("extid1", extension_id.value());
@@ -897,7 +901,7 @@
   }
   deltas.sort(&InDecreasingExtensionInstallationTimeOrder);
   ignored_actions.clear();
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_3, effective_new_url);
   EXPECT_EQ("extid3", extension_id.value());
@@ -915,7 +919,7 @@
   }
   deltas.sort(&InDecreasingExtensionInstallationTimeOrder);
   ignored_actions.clear();
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_3, effective_new_url);
   EXPECT_EQ("extid3", extension_id.value());
@@ -934,6 +938,10 @@
   GURL effective_new_url;
   std::optional<ExtensionId> extension_id;
 
+  WebRequestInfoInitParams info_params;
+  info_params.url = GURL(kExampleUrl);
+  WebRequestInfo request_info(std::move(info_params));
+
   // Single redirect.
   GURL new_url_0("http://foo.com");
   {
@@ -941,7 +949,7 @@
     d0.new_url = GURL(new_url_0);
     deltas.push_back(std::move(d0));
   }
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_0, effective_new_url);
   EXPECT_EQ("extid0", extension_id.value());
@@ -956,7 +964,7 @@
   }
   deltas.sort(&InDecreasingExtensionInstallationTimeOrder);
   ignored_actions.clear();
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_1, effective_new_url);
   EXPECT_EQ("extid1", extension_id.value());
@@ -973,7 +981,7 @@
   deltas.sort(&InDecreasingExtensionInstallationTimeOrder);
   ignored_actions.clear();
 
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_2, effective_new_url);
   EXPECT_EQ("extid2", extension_id.value());
@@ -989,7 +997,7 @@
   }
   deltas.sort(&InDecreasingExtensionInstallationTimeOrder);
   ignored_actions.clear();
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_1, effective_new_url);
   EXPECT_EQ("extid2", extension_id.value());
@@ -1006,6 +1014,10 @@
   GURL effective_new_url;
   std::optional<ExtensionId> extension_id;
 
+  WebRequestInfoInitParams info_params;
+  info_params.url = GURL(kExampleUrl);
+  WebRequestInfo request_info(std::move(info_params));
+
   // Single redirect.
   GURL new_url_0("http://foo.com");
   {
@@ -1013,7 +1025,7 @@
     d0.new_url = GURL(new_url_0);
     deltas.push_back(std::move(d0));
   }
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_0, effective_new_url);
   EXPECT_EQ("extid0", extension_id.value());
@@ -1028,14 +1040,14 @@
   }
   deltas.sort(&InDecreasingExtensionInstallationTimeOrder);
   ignored_actions.clear();
-  MergeOnBeforeRequestResponses(GURL(kExampleUrl), deltas, &effective_new_url,
+  MergeOnBeforeRequestResponses(request_info, deltas, &effective_new_url,
                                 &extension_id, &ignored_actions);
   EXPECT_EQ(new_url_1, effective_new_url);
   EXPECT_EQ("extid1", extension_id.value());
   EXPECT_TRUE(ignored_actions.empty());
 }
 
-// This tests that WebSocket requests can not be redirected.
+// This tests that WebSocket and WebTransport requests can not be redirected.
 TEST(ExtensionWebRequestHelpersTest, TestMergeOnBeforeRequestResponses4) {
   EventResponseDeltas deltas;
   helpers::IgnoredActions ignored_actions;
@@ -1048,9 +1060,21 @@
     delta.new_url = GURL("http://foo.com");
     deltas.push_back(std::move(delta));
   }
-  MergeOnBeforeRequestResponses(GURL("ws://example.com"), deltas,
-                                &effective_new_url, &extension_id,
-                                &ignored_actions);
+
+  WebRequestInfoInitParams ws_params;
+  ws_params.url = GURL("ws://example.com");
+  WebRequestInfo ws_info(std::move(ws_params));
+  MergeOnBeforeRequestResponses(ws_info, deltas, &effective_new_url,
+                                &extension_id, &ignored_actions);
+  EXPECT_EQ(GURL(), effective_new_url);
+  EXPECT_FALSE(extension_id.has_value());
+
+  WebRequestInfoInitParams wt_params;
+  wt_params.url = GURL("https://example.com");
+  wt_params.web_request_type = WebRequestResourceType::WEB_TRANSPORT;
+  WebRequestInfo wt_info(std::move(wt_params));
+  MergeOnBeforeRequestResponses(wt_info, deltas, &effective_new_url,
+                                &extension_id, &ignored_actions);
   EXPECT_EQ(GURL(), effective_new_url);
   EXPECT_FALSE(extension_id.has_value());
 }
diff --git a/extensions/browser/api/declarative_net_request/ruleset_matcher_unittest.cc b/extensions/browser/api/declarative_net_request/ruleset_matcher_unittest.cc
index c401ef0..0e83321 100644
--- a/extensions/browser/api/declarative_net_request/ruleset_matcher_unittest.cc
+++ b/extensions/browser/api/declarative_net_request/ruleset_matcher_unittest.cc
@@ -134,6 +134,45 @@
       matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest));
 }
 
+// Tests that redirect rules are not applied to WebSocket or WebTransport
+// handshakes since the underlying request stack does not support redirects for
+// these connection types.
+TEST_F(RulesetMatcherTest, RedirectIgnoredForWebSocketAndWebTransport) {
+  TestRule rule = CreateGenericRule();
+  rule.condition->url_filter = std::string("example.com");
+  rule.condition->resource_types =
+      std::vector<std::string>({"xmlhttprequest", "websocket", "webtransport"});
+  rule.priority = kMinValidPriority;
+  rule.action->type = std::string("redirect");
+  rule.action->redirect.emplace();
+  rule.action->redirect->url = std::string("https://other.com/");
+
+  std::unique_ptr<RulesetMatcher> matcher;
+  ASSERT_TRUE(CreateVerifiedMatcher({rule}, CreateTemporarySource(), &matcher));
+
+  GURL https_url("https://example.com/");
+  RequestParams params;
+  params.url = &https_url;
+  params.is_third_party = true;
+
+  // Check that the rule matches an ordinary request.
+  params.element_type = url_pattern_index::flat::ElementType_XMLHTTPREQUEST;
+  std::optional<RequestAction> action =
+      matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest);
+  ASSERT_TRUE(action);
+  EXPECT_EQ(RequestAction::Type::REDIRECT, action->type);
+
+  params.element_type = url_pattern_index::flat::ElementType_WEBTRANSPORT;
+  EXPECT_FALSE(
+      matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest));
+
+  GURL wss_url("wss://example.com/");
+  params.url = &wss_url;
+  params.element_type = url_pattern_index::flat::ElementType_WEBSOCKET;
+  EXPECT_FALSE(
+      matcher->GetAction(params, RulesetMatchingStage::kOnBeforeRequest));
+}
+
 // Tests a simple upgrade scheme rule.
 TEST_F(RulesetMatcherTest, UpgradeRule) {
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

Bypass of Declarative Net Request and webRequest redirect rules for WebTransport

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A logic error in the browser process allows WebTransport handshakes to bypass extension-initiated redirect actions while erroneously reporting them as successfully applied. This causes a silent failure of security and filtering features, such as ad-blocking sinkholes or enterprise policy enforcement, when WebTransport is used instead of Fetch or WebSockets.

Affected files:

  • extensions/browser/api/web_request/web_request_proxying_webtransport.cc
  • extensions/browser/api/declarative_net_request/ruleset_matcher_base.cc
  • extensions/browser/api/web_request/extension_web_request_event_router.cc
  • extensions/browser/api/web_request/web_request_api_helpers.cc

Estimated timestamp from git blame: 2021-08-26

Summary

A potential security bypass exists in the Declarative Net Request (DNR) and webRequest APIs where redirect actions matching WebTransport handshakes are not enforced, despite being reported as successful by extension audit tools and browser telemetry. This allow web content to bypass extension-based filtering or sinkholing by using WebTransport, while the extension remains unaware of the bypass.

Root Cause Analysis

The issue arises from a discrepancy between the rule-matching logic and the proxying implementation for WebTransport in the browser process:

  1. DNR Rule Matching: In extensions/browser/api/declarative_net_request/ruleset_matcher_base.cc, the method RulesetMatcherBase::CreateRedirectAction contains a check to suppress redirects for WebSockets (line 392) but fails to include a similar check for WebTransport (flat_rule::ElementType_WEBTRANSPORT). This allows a REDIRECT action to be generated for WebTransport requests.

  2. Telemetry False Positives: In extensions/browser/api/web_request/extension_web_request_event_router.cc, the OnBeforeRequest method evaluates matched rules and calls OnDNRActionMatched (line 1145) for redirect actions. This updates the extension’s action badge, populates the rule match in chrome.declarativeNetRequest.getMatchedRules(), and notifies Safe Browsing telemetry—all before the redirect is actually attempted.

  3. Proxy Implementation: In extensions/browser/api/web_request/web_request_proxying_webtransport.cc, the WebTransportHandshakeProxy calls OnBeforeRequest (line 77) and provides a redirect_url_ pointer. However, the implementation subsequently ignores any URL written to this pointer and continues the handshake to the original destination. A code comment at line 74 acknowledges this: “Since WebTransport doesn’t support redirect, ‘redirect_url’ is ignored even if extensions assigned it.”

Similar logic errors exist in the legacy webRequest helper MergeRedirectUrlOfResponsesHelper within extensions/browser/api/web_request/web_request_api_helpers.cc (line 938), which also fails to suppress redirects for WebTransport.

Potential Reproduction Steps

Note: These steps are based on static analysis of the source code.

  1. Install an extension with a DNR rule that redirects a specific target domain (e.g., ads.example.com) to a local sinkhole or data:, URL.
  2. Navigate to a page that initiates a WebTransport connection to the target domain: new WebTransport('https://ads.example.com:4433/').
  3. Observe that the connection successfully reaches ads.example.com in the network log.
  4. Observe that the extension’s action badge count has incremented and chrome.declarativeNetRequest.getMatchedRules() returns a match for the request, incorrectly indicating that the redirect was applied.

Suggested Fix

To prevent misleading telemetry and silent bypasses, WebTransport should be treated similarly to WebSockets in the extension APIs. Redirect actions for WebTransport should be explicitly disallowed or suppressed at the rule-matching stage in:

  • RulesetMatcherBase::CreateRedirectAction in ruleset_matcher_base.cc
  • MergeRedirectUrlOfResponsesHelper in web_request_api_helpers.cc

This ensures that if an extension attempts to redirect a WebTransport request, the action is correctly identified as unsupported or ignored at the API level, rather than reporting a false success.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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.

View on issue tracker
Links in the report