CVE-2026-10997
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
IN_PROC_BROWSER_TEST_Pchrome/browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.cc |
modified | |
ifchrome/browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.cc |
modified | |
TEST_Pchrome/browser/extensions/api/declarative_net_request/ruleset_manager_unittest.cc |
modified | |
ifchrome/browser/extensions/api/declarative_net_request/ruleset_manager_unittest.cc |
modified |
Files Changed
chrome/browser/extensions/api/declarative_net_request/declarative_net_request_browsertest.ccchrome/browser/extensions/api/declarative_net_request/ruleset_manager_unittest.cc
Patch
From 53caa53855606e2a8bc96efc30bc040f6350c9eb Mon Sep 17 00:00:00 2001 From: Kelvin Jiang <[email protected]> Date: Fri, 17 Apr 2026 16:56:54 -0700 Subject: [PATCH] [DNR] Require file access for file URL redirects Require file access to be enabled for the extension before their DNR rules (redirect) can operate on requests FROM file URLs, or if the redirect rule would redirect a request TO a file URL. Fixed: 464217867, 483777842 Change-Id: Ibad51967c28d13cf3f662265c9d61ccaefc4cc70 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7746569 Reviewed-by: Andrea Orru <[email protected]> Commit-Queue: Kelvin Jiang <[email protected]> Cr-Commit-Position: refs/heads/main@{#1616957} --- 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 f87076b..d51ecd7 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 @@ -42,6 +42,7 @@ #include "base/time/time.h" #include "base/values.h" #include "build/build_config.h" +#include "chrome/browser/extensions/chrome_test_extension_loader.h" #include "chrome/browser/extensions/extension_action_runner.h" #include "chrome/browser/extensions/extension_browsertest.h" #include "chrome/browser/extensions/extension_tab_util.h" @@ -126,6 +127,7 @@ #include "extensions/common/url_pattern_set.h" #include "extensions/test/extension_background_page_waiter.h" #include "extensions/test/extension_test_message_listener.h" +#include "net/base/filename_util.h" #include "net/base/net_errors.h" #include "net/dns/mock_host_resolver.h" #include "net/http/http_request_headers.h" @@ -9105,6 +9107,78 @@ const auto kExtensionLoadTypes = ::testing::Values(ExtensionLoadType::PACKED, ExtensionLoadType::UNPACKED); +#if !BUILDFLAG(IS_ANDROID) +// Tests that an extension must have local file access to redirect TO file URLs. +// Disabled on Android since it heavily discourages direct file scheme URL +// access. +IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest, FileUrlRedirect) { + base::ScopedAllowBlockingForTesting allow_blocking; + base::ScopedTempDir temp_dir; + ASSERT_TRUE(temp_dir.CreateUniqueTempDir()); + + base::FilePath file_path = temp_dir.GetPath().AppendASCII("test_file.html"); + ASSERT_TRUE(base::WriteFile(file_path, "success")); + GURL file_url = net::FilePathToFileURL(file_path); + + TestRule rule = CreateGenericRule(); + rule.id = kMinValidID; + rule.priority = kMinValidPriority; + rule.condition->url_filter = std::string("http://example.com/redirect"); + rule.action->type = std::string("redirect"); + rule.action->redirect.emplace(); + rule.action->redirect->url = file_url.spec(); + + auto run_test = [&](const std::string& ext_name, bool allow_file_access) { + base::FilePath extension_dir = temp_dir.GetPath().AppendASCII(ext_name); + EXPECT_TRUE(base::CreateDirectory(extension_dir)); + + TestRulesetInfo info("id", "rules_file.json", ToListValue({rule})); + WriteManifestAndRuleset(extension_dir, info, + {URLPattern::kAllUrlsPattern, "file:///*"}, + ConfigFlag::kConfig_None); + + // Write a simple HTML page in the extension directory. + EXPECT_TRUE(base::WriteFile(extension_dir.AppendASCII("page.html"), + "<html><body></body></html>")); + + ChromeTestExtensionLoader loader(profile()); + loader.set_allow_file_access(allow_file_access); + scoped_refptr<const Extension> extension = + loader.LoadExtension(extension_dir); + EXPECT_TRUE(extension); + + // Navigate to the extension's page so we have file access privileges if + // allowed. + GURL extension_page = extension->GetResourceURL("page.html"); + EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), extension_page)); + + // Inject an iframe navigating to http://example.com/redirect. + content::TestNavigationObserver observer(GetActiveWebContents(), 1); + ASSERT_TRUE( + content::ExecJs(GetPrimaryMainFrame(), + "const frame = document.createElement('iframe');\n" + "frame.src = 'http://example.com/redirect';\n" + "document.body.appendChild(frame);")); + observer.Wait(); + + // Check the iframe's URL. + content::RenderFrameHost* child_frame = + content::ChildFrameAt(GetPrimaryMainFrame(), 0); + ASSERT_TRUE(child_frame); + GURL iframe_url = child_frame->GetLastCommittedURL(); + + if (allow_file_access) { + EXPECT_EQ(file_url, iframe_url); + } else { + EXPECT_EQ(GURL("http://example.com/redirect"), iframe_url); + } + }; + + run_test("ext_denied", false); + run_test("ext_allowed", true); +} +#endif // !BUILDFLAG(IS_ANDROID) + INSTANTIATE_TEST_SUITE_P(All, DeclarativeNetRequestBrowserTest, ::testing::Combine(kExtensionLoadTypes, diff --git a/chrome/browser/extensions/api/declarative_net_request/ruleset_manager_unittest.cc b/chrome/browser/extensions/api/declarative_net_request/ruleset_manager_unittest.cc index 9c4f410..a593872 100644 --- a/chrome/browser/extensions/api/declarative_net_request/ruleset_manager_unittest.cc +++ b/chrome/browser/extensions/api/declarative_net_request/ruleset_manager_unittest.cc @@ -80,7 +80,8 @@ const std::string& extension_dirname, std::unique_ptr<CompositeMatcher>* matcher, const std::vector<std::string>& host_permissions = {}, - bool has_background_script = false) { + bool has_background_script = false, + bool allow_file_access = false) { base::FilePath extension_dir = temp_dir().GetPath().AppendASCII(extension_dirname); @@ -96,8 +97,9 @@ TestRulesetInfo info(kRulesetID, kJSONRulesFilename, ToListValue(rules)); WriteManifestAndRuleset(extension_dir, info, host_permissions, flags); - last_loaded_extension_ = - CreateExtensionLoader()->LoadExtension(extension_dir); + auto loader = CreateExtensionLoader(); + loader->set_allow_file_access(allow_file_access); + last_loaded_extension_ = loader->LoadExtension(extension_dir); ASSERT_TRUE(last_loaded_extension_); ExtensionRegistry::Get(browser_context()) @@ -1187,6 +1189,108 @@ } } +// Tests that an extension must have local file access to intercept requests +// from file URLs. +TEST_P(RulesetManagerTest, LocalFileAccess) { + TestRule block_rule = CreateGenericRule(); + block_rule.id = kMinValidID; + block_rule.priority = kMinValidPriority; + block_rule.condition->url_filter = std::string("file:///abc"); + block_rule.action->type = std::string("block"); + + TestRule redirect_rule = CreateGenericRule(); + redirect_rule.id = kMinValidID + 1; + redirect_rule.priority = kMinValidPriority + 1; + redirect_rule.condition->url_filter = std::string("file:///def"); + redirect_rule.action->type = std::string("redirect"); + redirect_rule.action->redirect.emplace(); + redirect_rule.action->redirect->url = std::string("http://google.com"); + + auto run_test = [&](const std::string& name, bool allow_file_access) { + std::unique_ptr<CompositeMatcher> matcher; + ASSERT_NO_FATAL_FAILURE(CreateMatcherForRules( + {block_rule, redirect_rule}, name, &matcher, + {URLPattern::kAllUrlsPattern}, + /*has_background_script=*/false, allow_file_access)); + const Extension* extension = last_loaded_extension(); + manager()->AddRuleset(extension->id(), std::move(matcher)); + + // Send two requests: one matching `block_rule` and one matching + // `redirect_rule`. + WebRequestInfo request_1(GetRequestParamsForURL("file:///abc")); + manager()->EvaluateBeforeRequest(request_1, + /*is_incognito_context=*/false); + + WebRequestInfo request_2(GetRequestParamsForURL("file:///def")); + manager()->EvaluateBeforeRequest(request_2, + /*is_incognito_context=*/false); + + if (allow_file_access) { + ASSERT_EQ(1u, request_1.dnr_actions->size()); + RequestAction expected_block = CreateRequestActionForTesting( + RequestActionType::BLOCK, *block_rule.id, *block_rule.priority, + kMinValidStaticRulesetID, extension->id()); + EXPECT_EQ(expected_block, (*request_1.dnr_actions)[0]); + + ASSERT_EQ(1u, request_2.dnr_actions->size()); + RequestAction expected_redirect = CreateRequestActionForTesting( + RequestActionType::REDIRECT, *redirect_rule.id, + *redirect_rule.priority, kMinValidStaticRulesetID, extension->id()); + expected_redirect.redirect_url = GURL("http://google.com"); + EXPECT_EQ(expected_redirect, (*request_2.dnr_actions)[0]); + } else { + // Neither request should be matched with the extension's rulesets so DNR + // actions should be empty. + EXPECT_TRUE(request_1.dnr_actions->empty()); + EXPECT_TRUE(request_2.dnr_actions->empty());
Regression Test / PoC
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 f87076b..d51ecd7 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
@@ -42,6 +42,7 @@
#include "base/time/time.h"
#include "base/values.h"
#include "build/build_config.h"
+#include "chrome/browser/extensions/chrome_test_extension_loader.h"
#include "chrome/browser/extensions/extension_action_runner.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/extensions/extension_tab_util.h"
@@ -126,6 +127,7 @@
#include "extensions/common/url_pattern_set.h"
#include "extensions/test/extension_background_page_waiter.h"
#include "extensions/test/extension_test_message_listener.h"
+#include "net/base/filename_util.h"
#include "net/base/net_errors.h"
#include "net/dns/mock_host_resolver.h"
#include "net/http/http_request_headers.h"
@@ -9105,6 +9107,78 @@
const auto kExtensionLoadTypes =
::testing::Values(ExtensionLoadType::PACKED, ExtensionLoadType::UNPACKED);
+#if !BUILDFLAG(IS_ANDROID)
+// Tests that an extension must have local file access to redirect TO file URLs.
+// Disabled on Android since it heavily discourages direct file scheme URL
+// access.
+IN_PROC_BROWSER_TEST_P(DeclarativeNetRequestBrowserTest, FileUrlRedirect) {
+ base::ScopedAllowBlockingForTesting allow_blocking;
+ base::ScopedTempDir temp_dir;
+ ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
+
+ base::FilePath file_path = temp_dir.GetPath().AppendASCII("test_file.html");
+ ASSERT_TRUE(base::WriteFile(file_path, "success"));
+ GURL file_url = net::FilePathToFileURL(file_path);
+
+ TestRule rule = CreateGenericRule();
+ rule.id = kMinValidID;
+ rule.priority = kMinValidPriority;
+ rule.condition->url_filter = std::string("http://example.com/redirect");
+ rule.action->type = std::string("redirect");
+ rule.action->redirect.emplace();
+ rule.action->redirect->url = file_url.spec();
+
+ auto run_test = [&](const std::string& ext_name, bool allow_file_access) {
+ base::FilePath extension_dir = temp_dir.GetPath().AppendASCII(ext_name);
+ EXPECT_TRUE(base::CreateDirectory(extension_dir));
+
+ TestRulesetInfo info("id", "rules_file.json", ToListValue({rule}));
+ WriteManifestAndRuleset(extension_dir, info,
+ {URLPattern::kAllUrlsPattern, "file:///*"},
+ ConfigFlag::kConfig_None);
+
+ // Write a simple HTML page in the extension directory.
+ EXPECT_TRUE(base::WriteFile(extension_dir.AppendASCII("page.html"),
+ "<html><body></body></html>"));
+
+ ChromeTestExtensionLoader loader(profile());
+ loader.set_allow_file_access(allow_file_access);
+ scoped_refptr<const Extension> extension =
+ loader.LoadExtension(extension_dir);
+ EXPECT_TRUE(extension);
+
+ // Navigate to the extension's page so we have file access privileges if
+ // allowed.
+ GURL extension_page = extension->GetResourceURL("page.html");
+ EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), extension_page));
+
+ // Inject an iframe navigating to http://example.com/redirect.
+ content::TestNavigationObserver observer(GetActiveWebContents(), 1);
+ ASSERT_TRUE(
+ content::ExecJs(GetPrimaryMainFrame(),
+ "const frame = document.createElement('iframe');\n"
+ "frame.src = 'http://example.com/redirect';\n"
+ "document.body.appendChild(frame);"));
+ observer.Wait();
+
+ // Check the iframe's URL.
+ content::RenderFrameHost* child_frame =
+ content::ChildFrameAt(GetPrimaryMainFrame(), 0);
+ ASSERT_TRUE(child_frame);
+ GURL iframe_url = child_frame->GetLastCommittedURL();
+
+ if (allow_file_access) {
+ EXPECT_EQ(file_url, iframe_url);
+ } else {
+ EXPECT_EQ(GURL("http://example.com/redirect"), iframe_url);
+ }
+ };
+
+ run_test("ext_denied", false);
+ run_test("ext_allowed", true);
+}
+#endif // !BUILDFLAG(IS_ANDROID)
+
INSTANTIATE_TEST_SUITE_P(All,
DeclarativeNetRequestBrowserTest,
::testing::Combine(kExtensionLoadTypes,
diff --git a/chrome/browser/extensions/api/declarative_net_request/ruleset_manager_unittest.cc b/chrome/browser/extensions/api/declarative_net_request/ruleset_manager_unittest.cc
index 9c4f410..a593872 100644
--- a/chrome/browser/extensions/api/declarative_net_request/ruleset_manager_unittest.cc
+++ b/chrome/browser/extensions/api/declarative_net_request/ruleset_manager_unittest.cc
@@ -80,7 +80,8 @@
const std::string& extension_dirname,
std::unique_ptr<CompositeMatcher>* matcher,
const std::vector<std::string>& host_permissions = {},
- bool has_background_script = false) {
+ bool has_background_script = false,
+ bool allow_file_access = false) {
base::FilePath extension_dir =
temp_dir().GetPath().AppendASCII(extension_dirname);
@@ -96,8 +97,9 @@
TestRulesetInfo info(kRulesetID, kJSONRulesFilename, ToListValue(rules));
WriteManifestAndRuleset(extension_dir, info, host_permissions, flags);
- last_loaded_extension_ =
- CreateExtensionLoader()->LoadExtension(extension_dir);
+ auto loader = CreateExtensionLoader();
+ loader->set_allow_file_access(allow_file_access);
+ last_loaded_extension_ = loader->LoadExtension(extension_dir);
ASSERT_TRUE(last_loaded_extension_);
ExtensionRegistry::Get(browser_context())
@@ -1187,6 +1189,108 @@
}
}
+// Tests that an extension must have local file access to intercept requests
+// from file URLs.
+TEST_P(RulesetManagerTest, LocalFileAccess) {
+ TestRule block_rule = CreateGenericRule();
+ block_rule.id = kMinValidID;
+ block_rule.priority = kMinValidPriority;
+ block_rule.condition->url_filter = std::string("file:///abc");
+ block_rule.action->type = std::string("block");
+
+ TestRule redirect_rule = CreateGenericRule();
+ redirect_rule.id = kMinValidID + 1;
+ redirect_rule.priority = kMinValidPriority + 1;
+ redirect_rule.condition->url_filter = std::string("file:///def");
+ redirect_rule.action->type = std::string("redirect");
+ redirect_rule.action->redirect.emplace();
+ redirect_rule.action->redirect->url = std::string("http://google.com");
+
+ auto run_test = [&](const std::string& name, bool allow_file_access) {
+ std::unique_ptr<CompositeMatcher> matcher;
+ ASSERT_NO_FATAL_FAILURE(CreateMatcherForRules(
+ {block_rule, redirect_rule}, name, &matcher,
+ {URLPattern::kAllUrlsPattern},
+ /*has_background_script=*/false, allow_file_access));
+ const Extension* extension = last_loaded_extension();
+ manager()->AddRuleset(extension->id(), std::move(matcher));
+
+ // Send two requests: one matching `block_rule` and one matching
+ // `redirect_rule`.
+ WebRequestInfo request_1(GetRequestParamsForURL("file:///abc"));
+ manager()->EvaluateBeforeRequest(request_1,
+ /*is_incognito_context=*/false);
+
+ WebRequestInfo request_2(GetRequestParamsForURL("file:///def"));
+ manager()->EvaluateBeforeRequest(request_2,
+ /*is_incognito_context=*/false);
+
+ if (allow_file_access) {
+ ASSERT_EQ(1u, request_1.dnr_actions->size());
+ RequestAction expected_block = CreateRequestActionForTesting(
+ RequestActionType::BLOCK, *block_rule.id, *block_rule.priority,
+ kMinValidStaticRulesetID, extension->id());
+ EXPECT_EQ(expected_block, (*request_1.dnr_actions)[0]);
+
+ ASSERT_EQ(1u, request_2.dnr_actions->size());
+ RequestAction expected_redirect = CreateRequestActionForTesting(
+ RequestActionType::REDIRECT, *redirect_rule.id,
+ *redirect_rule.priority, kMinValidStaticRulesetID, extension->id());
+ expected_redirect.redirect_url = GURL("http://google.com");
+ EXPECT_EQ(expected_redirect, (*request_2.dnr_actions)[0]);
+ } else {
+ // Neither request should be matched with the extension's rulesets so DNR
+ // actions should be empty.
+ EXPECT_TRUE(request_1.dnr_actions->empty());
+ EXPECT_TRUE(request_2.dnr_actions->empty());
+ }
+ };
+
+ run_test("ext_denied", false);
+ run_test("ext_allowed", true);
+}
+
+// Tests that an extension must have local file access to redirect TO file URLs.
+TEST_P(RulesetManagerTest, RedirectToFileUrl) {
+ TestRule redirect_rule = CreateGenericRule();
+ redirect_rule.id = kMinValidID + 1;
+ redirect_rule.priority = kMinValidPriority + 1;
+ redirect_rule.condition->url_filter = std::string("http://example.com/xyz");
+ redirect_rule.action->type = std::string("redirect");
+ redirect_rule.action->redirect.emplace();
+ redirect_rule.action->redirect->url = std::string("file:///def");
+
+ auto run_test = [&](const std::string& name, bool allow_file_access) {
+ std::unique_ptr<CompositeMatcher> matcher;
+ ASSERT_NO_FATAL_FAILURE(CreateMatcherForRules(
+ {redirect_rule}, name, &matcher, {URLPattern::kAllUrlsPattern},
+ /*has_background_script=*/false, allow_file_access));
+ const Extension* extension = last_loaded_extension();
+ manager()->AddRuleset(extension->id(), std::move(matcher));
+
+ WebRequestInfo request(GetRequestParamsForURL("http://example.com/xyz"));
+ const std::vector<RequestAction>& actions =
+ manager()->EvaluateBeforeRequest(request,
+ /*is_incognito_context=*/false);
+
+ if (allow_file_access) {
+ ASSERT_EQ(1u, actions.size());
+ RequestAction expected_redirect = CreateRequestActionForTesting(
+ RequestActionType::REDIRECT, *redirect_rule.id,
+ *redirect_rule.priority, kMinValidStaticRulesetID, extension->id());
+ expected_redirect.redirect_url = GURL("file:///def");
+ EXPECT_EQ(expected_redirect, actions[0]);
+ } else {
+ // No actions should be matched if the extension does not have file
+ // access.
+ EXPECT_TRUE(actions.empty());
+ }
+ };
+
+ run_test("ext_denied", false);
+ run_test("ext_allowed", true);
+}
+
INSTANTIATE_TEST_SUITE_P(All,
RulesetManagerTest,
::testing::Values(ExtensionLoadType::PACKED,
Original Bug Report
Insufficient URL Scheme Validation in regex_rules_matcher.cc
URL: src/extensions/browser/api/declarative_net_request/regex_rules_matcher.cc
Details
“source_file”: “src/extensions/browser/api/declarative_net_request/regex_rules_matcher.cc”, “type”: “Insufficient URL Scheme Validation”
The CreateRegexSubstitutionRedirectAction function, which handles declarativeNetRequest redirect rules with regex substitutions, fails to properly validate the scheme of the generated redirect URL. While it correctly blocks javascript: URLs, it does not prevent redirects to other dangerous schemes, most notably file:. This allows a malicious extension with the declarativeNetRequest permission to craft a rule that redirects a user’s navigation to a file: URL. This URL can open internal files ,
location :
std::optional<RequestAction>\nRegexRulesMatcher::CreateRegexSubstitutionRedirectAction(\n const RequestParams& params,\n const RegexRuleInfo& info) const {\n // …\n GURL redirect_url(redirect_str);\n\n // Redirects to JavaScript urls are not allowed.\n // TODO(crbug.com/40111509): this results in counterintuitive behavior.\n if (redirect_url.SchemeIs(url::kJavaScriptScheme)) {\n return std::nullopt;\n }\n\n return CreateRedirectAction(params, *info.regex_rule->url_rule(),\n std::move(redirect_url));\n}
#impact:
i was able to downlaod html file and open it with file:/// direclty , also can use that vuln with other schema
Impact A malicious Chrome extension can download a file and then access it via a file:/// URL. This behavior allows extensions to open or read local files that should be protected by Chrome’s isolation of local file resources. An attacker can use any URL scheme and open nearly any file type on disk.
Important — dynamic rules.json The rules.json used in this attack is dynamic and generated at runtime after the extension is installed. It is not a static file shipped inside the extension package or stored in the extension folder. Because the redirect rules are created on-demand (after installation) and retrieved from the attacker’s server, Web Store reviewers performing a static inspection of the extension bundle will not see these malicious rules. This means the attack cannot be detected simply by reviewing the extension package contents on the Web Store.
I’ve attached a video and PoC files.
Steps to reproduce
Download the PoC files.
Start attacker_server.py.
Load the malicious extension into Chrome.
The extension generates file.html (which contains an alert in the PoC) and automatically downloads it to Chrome’s default download folder.
The extension detects where file.html was saved and sends that path to the attacker server.
The attacker server generates a dynamic rules.json (not present in the extension bundle) and returns it to the extension.
The extension updates its declarativeNetRequest rules with that rules.json.
The extension automatically opens a window or popup with a host such as example.com. The dynamic rules.json redirects example.com to file:///path/file.html so the local file is loaded and executed in the browser context. Attack scenario Attack scenario
Result file:///path/file.html is loaded and executed in the browser, allowing execution of local-file content or access to local resources that should not be accessible. Because the redirect rules are created and deployed at runtime, the malicious behavior is not visible by inspecting the extension bundle or a static Web Store review.