Chrome · Extensions › Extensions
CVE-2026-113272
Memory Corruption in Extensions (Extensions)
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/extensions/api/declarative_content/content_action.cc |
modified | |
is_declarative_extensions/renderer/user_script_injector.cc |
modified | |
script_type_extensions/renderer/user_script_injector.cc |
modified | |
TEST_Fchrome/browser/extensions/extension_action_runner_unittest.cc |
modified |
Files Changed
chrome/browser/extensions/extension_action_runner_unittest.ccextensions/renderer/user_script_injector.ccchrome/browser/extensions/api/declarative_content/content_action.ccextensions/browser/script_injection_tracker.hextensions/renderer/user_script_set.ccextensions/browser/script_injection_tracker.ccchrome/browser/extensions/extension_action_runner.ccextensions/common/mojom/injection_type.mojomextensions/renderer/user_script_injector.hchrome/browser/extensions/api/declarative_content/request_content_script_apitest.cc
Patch
From 1f283860b8dbf805bb5e8ed89e8825d6bcc8c30f Mon Sep 17 00:00:00 2001 From: Justin Lulejian <[email protected]> Date: Wed, 30 Sep 2026 09:34:15 -0700 Subject: [PATCH] [Extensions] Fix ScriptInjectionTracker desync for RequestContentScript Previously, RequestContentScript::InstructRenderProcessToInject() used PermissionsData::CanAccessPage() to check if an extension could inject a declarative content script into a frame. Because CanAccessPage() returns true even when host permissions are withheld, the browser invoked ScriptInjectionTracker::WillExecuteCode() prematurely before sending ExecuteDeclarativeScript() to the renderer. In the renderer, ScriptInjection::TryToInject() detected that permissions were withheld and deferred execution. This caused the browser and renderer states to desynchronize: the browser erroneously considered the renderer process as having executed the extension's content script even though no script ran. With this change, the browser does not update ScriptInjectionTracker when page access is withheld during declarative content script evaluation. Instead, the renderer defers the script and requests permission via ExtensionActionRunner. Once the user explicitly grants permission on the tab, the deferred script executes in the renderer and ScriptInjectionTracker is updated so browser and renderer states remain synchronized. We update RequestContentScript::InstructRenderProcessToInject() to query PermissionsData::GetPageAccess() directly and only call ScriptInjectionTracker::WillExecuteCode() when access is kAllowed. We also add mojom::InjectionType::kDeclarativeScript, which UserScriptInjector reports for RequestContentScript scripts, and have ExtensionActionRunner gate it on PermissionsData::GetPageAccess() so withheld requests are queued. ExtensionActionRunner already notifies ScriptInjectionTracker before permitting pending scripts to execute. Fixed: 513486355 Change-Id: Ic8a5e4b503f33759d75b440f782d9fde6a549fae Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8417402 Reviewed-by: Dominic Farolino <[email protected]> Reviewed-by: Tim <[email protected]> Commit-Queue: Justin Lulejian <[email protected]> Auto-Submit: Justin Lulejian <[email protected]> Cr-Commit-Position: refs/heads/main@{#1708194} --- diff --git a/chrome/browser/extensions/api/declarative_content/content_action.cc b/chrome/browser/extensions/api/declarative_content/content_action.cc index fed023b..2efa3c8 100644 --- a/chrome/browser/extensions/api/declarative_content/content_action.cc +++ b/chrome/browser/extensions/api/declarative_content/content_action.cc @@ -377,16 +377,20 @@ void RequestContentScript::InstructRenderProcessToInject( content::WebContents* contents, const Extension* extension) const { - // Verify that the extension has permission to access the page before - // granting trust for script injection. This prevents a compromised renderer - // from fully bypassing permission checks (for example: a spoofed - // `extensions::mojom::LocalFrameHost::WatchedPageChange` IPC bypassing the - // check we have for an invalid selector and getting here). + // Bail out if the extension cannot access the page at all. This prevents a + // compromised renderer from fully bypassing permission checks (for example: a + // spoofed `extensions::mojom::LocalFrameHost::WatchedPageChange` IPC + // bypassing the check we have for an invalid selector and getting here). + // When access is withheld, the script is still sent to the renderer, which + // defers the injection and requests permission from the browser. Trust is + // only granted to the renderer process below when access is allowed. std::string error; content::RenderFrameHost* main_frame = contents->GetPrimaryMainFrame(); const GURL& url = util::GetURLForExtensionPermissionCheck(main_frame); - if (!extension->permissions_data()->CanAccessPage( - url, ExtensionTabUtil::GetTabId(contents), &error)) { + PermissionsData::PageAccess page_access = + extension->permissions_data()->GetPageAccess( + url, ExtensionTabUtil::GetTabId(contents), &error); + if (page_access == PermissionsData::PageAccess::kDenied) { return; } @@ -399,8 +403,13 @@ return; } - ScriptInjectionTracker::WillExecuteCode(base::PassKey<RequestContentScript>(), - main_frame, *extension); + // When access is withheld, `ScriptInjectionTracker` is instead updated by + // `ExtensionActionRunner` if the user later grants access and the deferred + // script is permitted to run. + if (page_access == PermissionsData::PageAccess::kAllowed) { + ScriptInjectionTracker::WillExecuteCode( + base::PassKey<RequestContentScript>(), main_frame, *extension); + } local_frame->ExecuteDeclarativeScript( sessions::SessionTabHelper::IdForTab(contents).id(), extension->id(), script_.id(), url); diff --git a/chrome/browser/extensions/extension_action_runner.cc b/chrome/browser/extensions/extension_action_runner.cc index 2025f3a2..4773b5d4 100644 --- a/chrome/browser/extensions/extension_action_runner.cc +++ b/chrome/browser/extensions/extension_action_runner.cc @@ -275,6 +275,10 @@ return extension->permissions_data()->GetContentScriptAccess(url, tab_id, nullptr); case mojom::InjectionType::kProgrammaticScript: + // Declarative scripts are gated on the extension's host permissions rather + // than its manifest content script matches, the same as + // `RequestContentScript::InstructRenderProcessToInject()`. + case mojom::InjectionType::kDeclarativeScript: return extension->permissions_data()->GetPageAccess(url, tab_id, nullptr); } diff --git a/extensions/browser/script_injection_tracker.cc b/extensions/browser/script_injection_tracker.cc index 0fa5fba..10c09d7b 100644 --- a/extensions/browser/script_injection_tracker.cc +++ b/extensions/browser/script_injection_tracker.cc @@ -850,9 +850,9 @@ ChromeTrackEvent::kChromeExtensionId, ExtensionIdForTracing(extension.id())); - // A programmatic injection that was deferred while host access was withheld - // is being permitted to run; this is treated like any other programmatic - // content script injection. + // A programmatic or declarative injection that was deferred while host access + // was withheld is being permitted to run; this is treated like any other + // programmatic content script injection. HandleProgrammaticScriptInjection(PassKey(), ScriptType::kContentScript, frame, extension); } diff --git a/extensions/browser/script_injection_tracker.h b/extensions/browser/script_injection_tracker.h index 8f0ba4c..9066b254 100644 --- a/extensions/browser/script_injection_tracker.h +++ b/extensions/browser/script_injection_tracker.h @@ -135,9 +135,9 @@ content::RenderFrameHost* frame, const Extension& extension); - // Called before a programmatic injection that was previously deferred - // (because host access was withheld) is permitted to run as a result of the - // user granting the extension access to the page. + // Called before a programmatic or declarative injection that was previously + // deferred (because host access was withheld) is permitted to run as a result + // of the user granting the extension access to the page. static void WillExecuteCode(base::PassKey<ExtensionActionRunner> pass_key, mojom::InjectionType script_type, content::RenderFrameHost* frame, diff --git a/extensions/common/mojom/injection_type.mojom b/extensions/common/mojom/injection_type.mojom index 1f83588..357ac61 100644 --- a/extensions/common/mojom/injection_type.mojom +++ b/extensions/common/mojom/injection_type.mojom @@ -10,4 +10,7 @@ kContentScript, // A script injected via, e.g. tabs.executeScript(). kProgrammaticScript, + // A script injected via the `RequestContentScript` action of the + // `chrome.declarativeContent` API. + kDeclarativeScript, }; diff --git a/extensions/renderer/user_script_injector.cc b/extensions/renderer/user_script_injector.cc index 1e639bf..766e2529 100644 --- a/extensions/renderer/user_script_injector.cc +++ b/extensions/renderer/user_script_injector.cc @@ -122,12 +122,14 @@ UserScriptInjector::UserScriptInjector(const UserScript* script, UserScriptSet* script_list, - bool is_declarative) + bool is_declarative, + mojom::InjectionType script_type) : script_(script), user_script_set_(script_list), script_id_(script_->id()), host_id_(script_->host_id()), - is_declarative_(is_declarative) { + is_declarative_(is_declarative), + script_type_(script_type) { user_script_set_observation_.Observe(script_list); } @@ -149,7 +151,7 @@ } mojom::InjectionType UserScriptInjector::script_type() const { - return mojom::InjectionType::kContentScript; + return script_type_; } blink::mojom::UserActivationOption UserScriptInjector::IsUserGesture() const { diff --git a/extensions/renderer/user_script_injector.h b/extensions/renderer/user_script_injector.h index 8a5205b..0c9c2e56 100644 --- a/extensions/renderer/user_script_injector.h +++ b/extensions/renderer/user_script_injector.h @@ -32,7 +32,8 @@ public: UserScriptInjector(const UserScript* user_script, UserScriptSet* user_script_set, - bool is_declarative); + bool is_declarative, + mojom::InjectionType script_type); UserScriptInjector(const UserScriptInjector&) = delete; UserScriptInjector& operator=(const UserScriptInjector&) = delete; @@ -94,9 +95,21 @@ mojom::HostID host_id_; // Indicates whether or not this script is declarative. This influences which - // script permissions are checked before injection. + // script permissions are checked before injection. This is also true for + // extension dynamic scripts (e.g. from `scripting.registerContentScripts()` + // or the `userScripts` API), which use host permissions but are still + // reported to the browser as `mojom::InjectionType::kContentScript` via + // `script_type_`. bool is_declarative_; + // The injection type reported to the browser, e.g. when requesting + // permission to inject via + // `extensions::mojom::LocalFrameHost::RequestScriptInjectionPermission()`. + // Only scripts injected via the `RequestContentScript` action of the + // `chrome.declarativeContent` API are reported as + // `mojom::InjectionType::kDeclarativeScript`. + const mojom::InjectionType script_type_; + base::ScopedObservation<UserScriptSet, UserScriptSet::Observer> user_script_set_observation_{this}; }; diff --git a/extensions/renderer/user_script_set.cc b/extensions/renderer/user_script_set.cc index fcec9315..fba35625 100644 --- a/extensions/renderer/user_script_set.cc +++ b/extensions/renderer/user_script_set.cc @@ -23,6 +23,7 @@ #include "extensions/common/extension_features.h" #include "extensions/common/extensions_client.h" #include "extensions/common/mojom/host_id.mojom.h" +#include "extensions/common/mojom/injection_type.mojom-shared.h" #include "extensions/renderer/extension_injection_host.h" #include "extensions/renderer/extensions_renderer_client.h" #include "extensions/renderer/injection_host.h" @@ -287,8 +288,18 @@ (host_id_.type == mojom::HostID::HostType::kExtensions) && (script->GetSource() == UserScript::Source::kDynamicContentScript || script->GetSource() == UserScript::Source::kDynamicUserScript); + // Only scripts injected via the `RequestContentScript` action of the + // `chrome.declarativeContent` API are reported as `kDeclarativeScript`. + // Extension dynamic scripts are still reported as `kContentScript` so that + // they keep content script behavior (e.g. asynchronous execution at + // `document_end` and `document_idle`). + mojom::InjectionType script_type = + is_declarative ? mojom::InjectionType::kDeclarativeScript + : mojom::InjectionType::kContentScript; std::unique_ptr<ScriptInjector> injector(new UserScriptInjector( - script, this, is_declarative || is_extension_dynamic_script)); + script, /*user_script_set=*/this, + /*is_declarative=*/is_declarative || is_extension_dynamic_script, + script_type)); if (injector->CanExecuteOnFrame(injection_host.get(), web_frame, tab_id) == PermissionsData::PageAccess::kDenied) { diff --git a/chrome/browser/extensions/api/declarative_content/request_content_script_apitest.cc b/chrome/browser/extensions/api/declarative_content/request_content_script_apitest.cc index 806f57d..2c1b2f9 100644 --- a/chrome/browser/extensions/api/declarative_content/request_content_script_apitest.cc +++ b/chrome/browser/extensions/api/declarative_content/request_content_script_apitest.cc @@ -5,6 +5,8 @@ #include "base/files/file_path.h" #include "base/memory/raw_ptr.h" #include "base/strings/stringprintf.h" +#include "chrome/browser/extensions/blocked_action_waiter.h" +#include "chrome/browser/extensions/extension_action_runner.h" #include "chrome/browser/extensions/extension_browsertest.h" #include "chrome/browser/extensions/extension_tab_util.h" #include "chrome/browser/profiles/profile.h" @@ -242,32 +244,25 @@ // Tests that when an extension's host permissions are withheld (e.g., site // access set to "On click"), triggering a declarative content rule with -// `RequestContentScript` should not record script execution in -// `ScriptInjectionTracker`. +// `RequestContentScript` does not record script execution in +// `ScriptInjectionTracker` until the user explicitly grants permission. // -// The browser and renderer states should stay synchronized regarding whether -// extension code has executed. Without proper gating, a bug causes these states -// to desynchronize: // In the browser process, // `RequestContentScript::InstructRenderProcessToInject()` checks -// `PermissionsData::CanAccessPage()`, which returns `true` even when host -// permissions are withheld (`PermissionsData::PageAccess::kWithheld`). This -// causes the browser to invoke `ScriptInjectionTracker::WillExecuteCode()`, -// erroneously marking the renderer process as having executed the content -// script. -// In the renderer process, -// `extensions::mojom::LocalFrame::ExecuteDeclarativeScript()` detects that host -// permissions are withheld and defers injection without running any script. +// `PermissionsData::GetPageAccess()`. When page access is +// `PermissionsData::PageAccess::kWithheld`, the browser sends +// `extensions::mojom::LocalFrame::ExecuteDeclarativeScript()` to the renderer +// without calling `ScriptInjectionTracker::WillExecuteCode()`. In the renderer, +// `ScriptInjection::TryToInject()` detects that permissions are withheld, +// defers the injection, and requests permission from the browser via +// `extensions::mojom::LocalFrameHost::RequestScriptInjectionPermission()`. // -// As a result of this bug, the browser process considers the renderer -// authorized to act on behalf of the extension, even though no extension code -// ever executed in that renderer. -// -// Currently, this test documents the existing buggy behavior by expecting -// `ScriptInjectionTracker::DidProcessRunContentScriptFromExtension()` to return -// `true` so the test passes before the fix. +// Once the user grants permission (simulated via +// `ExtensionActionRunner::RunForTesting()`), the deferred content script +// executes in the renderer and `ScriptInjectionTracker` records that the +// process ran a content script from the extension. IN_PROC_BROWSER_TEST_F(RequestContentScriptAPITest, - WithheldPermissionsPrematurelyUpdatesTracker) { + WithheldPermissionsUpdateTrackerOnlyAfterGrant) { // Start the embedded test server to serve test pages. ASSERT_TRUE(embedded_test_server()->Start()); @@ -282,24 +277,31 @@ .SetWithholdHostPermissions(/*withhold=*/true); // Set up a listener for script execution and navigate to a test URL on the - // embedded test server. + // embedded test server, waiting for the renderer to request permission for + // the deferred injection. content::WebContents* web_contents = GetActiveWebContents(); ASSERT_TRUE(web_contents); + ExtensionActionRunner* action_runner = + ExtensionActionRunner::GetForWebContents(web_contents); + ASSERT_TRUE(action_runner); + ExtensionTestMessageListener script_listener(kInjectionSucceeded); script_listener.set_extension_id(extension()->id()); const GURL target_url = embedded_test_server()->GetURL("/extensions/test_file.html"); - ASSERT_TRUE(NavigateToURL(web_contents, target_url)); + { + BlockedActionWaiter blocked_action_waiter(action_runner); + ASSERT_TRUE(NavigateToURL(web_contents, target_url)); + SCOPED_TRACE("Waiting for the deferred injection to request permission"); + blocked_action_waiter.Wait(); + } content::RenderProcessHost* target_process = web_contents->GetPrimaryMainFrame()->GetProcess(); ASSERT_TRUE(target_process); - // Run pending tasks in renderer to allow any potential injection to complete. - ASSERT_TRUE(RunAllPendingInRenderer(web_contents)); - // Verify that the extension's page access is withheld on the target URL. EXPECT_EQ(PermissionsData::PageAccess::kWithheld, extension()->permissions_data()->GetPageAccess( @@ -309,20 +311,21 @@ // Verify that the content script was not executed in the renderer. EXPECT_FALSE(script_listener.was_satisfied()); - // Verify that `ScriptInjectionTracker` records that the process ran a content - // script from this extension. This currently returns `true` because of the - // bug where `PermissionsData::CanAccessPage()` returns `true` for withheld - // permissions, erroneously notifying `ScriptInjectionTracker` before sending - // the injection message to the renderer. - // - // TODO(crbug.com/513486355): Once the bug is fixed in - // `RequestContentScript::InstructRenderProcessToInject()`, rename this test - // to `WithheldPermissionsDoNotUpdateTracker` and update this expectation to - // verify that `ScriptInjectionTracker` does not record execution when - // permissions are withheld: - // EXPECT_FALSE( - // ScriptInjectionTracker::DidProcessRunContentScriptFromExtension( - // *target_process, extension()->id())); + // Verify that `ScriptInjectionTracker` does not record that the process ran a + // content script from this extension while permissions are withheld. + EXPECT_FALSE(ScriptInjectionTracker::DidProcessRunContentScriptFromExtension( + *target_process, extension()->id())); + + // Grant permission to run the blocked action on the active tab and verify + // that the deferred content script executes and updates the tracker. + ASSERT_TRUE(action_runner->WantsToRun(extension())); + action_runner->RunForTesting(extension()); + + { + SCOPED_TRACE("Waiting for content script to execute after granting access"); + ASSERT_TRUE(script_listener.WaitUntilSatisfied()); + } + EXPECT_TRUE(ScriptInjectionTracker::DidProcessRunContentScriptFromExtension( *target_process, extension()->id())); } diff --git a/chrome/browser/extensions/extension_action_runner_unittest.cc b/chrome/browser/extensions/extension_action_runner_unittest.cc index ef5798d..5cf2eea 100644 --- a/chrome/browser/extensions/extension_action_runner_unittest.cc +++ b/chrome/browser/extensions/extension_action_runner_unittest.cc @@ -585,4 +585,34 @@ EXPECT_FALSE(runner()->WantsToRun(extension)); } +// Tests that `ExtensionActionRunner::RequiresUserConsentForScriptInjection()` +// gates `mojom::InjectionType::kDeclarativeScript` injections (from the +// `RequestContentScript` action of the `chrome.declarativeContent` API) on the +// extension's host permissions (`PermissionsData::GetPageAccess()`), the same +// as `mojom::InjectionType::kProgrammaticScript`, rather than on its manifest +// content script matches (`PermissionsData::GetContentScriptAccess()`). +TEST_F(ExtensionActionRunnerUnitTest, DeclarativeScriptsUseHostPermissions) { + // Add an extension with withheld all hosts permissions and no manifest ... (file diff truncated)
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/extensions/api/declarative_content/request_content_script_apitest.cc b/chrome/browser/extensions/api/declarative_content/request_content_script_apitest.cc
index 806f57d..2c1b2f9 100644
--- a/chrome/browser/extensions/api/declarative_content/request_content_script_apitest.cc
+++ b/chrome/browser/extensions/api/declarative_content/request_content_script_apitest.cc
@@ -5,6 +5,8 @@
#include "base/files/file_path.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/stringprintf.h"
+#include "chrome/browser/extensions/blocked_action_waiter.h"
+#include "chrome/browser/extensions/extension_action_runner.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/browser/extensions/extension_tab_util.h"
#include "chrome/browser/profiles/profile.h"
@@ -242,32 +244,25 @@
// Tests that when an extension's host permissions are withheld (e.g., site
// access set to "On click"), triggering a declarative content rule with
-// `RequestContentScript` should not record script execution in
-// `ScriptInjectionTracker`.
+// `RequestContentScript` does not record script execution in
+// `ScriptInjectionTracker` until the user explicitly grants permission.
//
-// The browser and renderer states should stay synchronized regarding whether
-// extension code has executed. Without proper gating, a bug causes these states
-// to desynchronize:
// In the browser process,
// `RequestContentScript::InstructRenderProcessToInject()` checks
-// `PermissionsData::CanAccessPage()`, which returns `true` even when host
-// permissions are withheld (`PermissionsData::PageAccess::kWithheld`). This
-// causes the browser to invoke `ScriptInjectionTracker::WillExecuteCode()`,
-// erroneously marking the renderer process as having executed the content
-// script.
-// In the renderer process,
-// `extensions::mojom::LocalFrame::ExecuteDeclarativeScript()` detects that host
-// permissions are withheld and defers injection without running any script.
+// `PermissionsData::GetPageAccess()`. When page access is
+// `PermissionsData::PageAccess::kWithheld`, the browser sends
+// `extensions::mojom::LocalFrame::ExecuteDeclarativeScript()` to the renderer
+// without calling `ScriptInjectionTracker::WillExecuteCode()`. In the renderer,
+// `ScriptInjection::TryToInject()` detects that permissions are withheld,
+// defers the injection, and requests permission from the browser via
+// `extensions::mojom::LocalFrameHost::RequestScriptInjectionPermission()`.
//
-// As a result of this bug, the browser process considers the renderer
-// authorized to act on behalf of the extension, even though no extension code
-// ever executed in that renderer.
-//
-// Currently, this test documents the existing buggy behavior by expecting
-// `ScriptInjectionTracker::DidProcessRunContentScriptFromExtension()` to return
-// `true` so the test passes before the fix.
+// Once the user grants permission (simulated via
+// `ExtensionActionRunner::RunForTesting()`), the deferred content script
+// executes in the renderer and `ScriptInjectionTracker` records that the
+// process ran a content script from the extension.
IN_PROC_BROWSER_TEST_F(RequestContentScriptAPITest,
- WithheldPermissionsPrematurelyUpdatesTracker) {
+ WithheldPermissionsUpdateTrackerOnlyAfterGrant) {
// Start the embedded test server to serve test pages.
ASSERT_TRUE(embedded_test_server()->Start());
@@ -282,24 +277,31 @@
.SetWithholdHostPermissions(/*withhold=*/true);
// Set up a listener for script execution and navigate to a test URL on the
- // embedded test server.
+ // embedded test server, waiting for the renderer to request permission for
+ // the deferred injection.
content::WebContents* web_contents = GetActiveWebContents();
ASSERT_TRUE(web_contents);
+ ExtensionActionRunner* action_runner =
+ ExtensionActionRunner::GetForWebContents(web_contents);
+ ASSERT_TRUE(action_runner);
+
ExtensionTestMessageListener script_listener(kInjectionSucceeded);
script_listener.set_extension_id(extension()->id());
const GURL target_url =
embedded_test_server()->GetURL("/extensions/test_file.html");
- ASSERT_TRUE(NavigateToURL(web_contents, target_url));
+ {
+ BlockedActionWaiter blocked_action_waiter(action_runner);
+ ASSERT_TRUE(NavigateToURL(web_contents, target_url));
+ SCOPED_TRACE("Waiting for the deferred injection to request permission");
+ blocked_action_waiter.Wait();
+ }
content::RenderProcessHost* target_process =
web_contents->GetPrimaryMainFrame()->GetProcess();
ASSERT_TRUE(target_process);
- // Run pending tasks in renderer to allow any potential injection to complete.
- ASSERT_TRUE(RunAllPendingInRenderer(web_contents));
-
// Verify that the extension's page access is withheld on the target URL.
EXPECT_EQ(PermissionsData::PageAccess::kWithheld,
extension()->permissions_data()->GetPageAccess(
@@ -309,20 +311,21 @@
// Verify that the content script was not executed in the renderer.
EXPECT_FALSE(script_listener.was_satisfied());
- // Verify that `ScriptInjectionTracker` records that the process ran a content
- // script from this extension. This currently returns `true` because of the
- // bug where `PermissionsData::CanAccessPage()` returns `true` for withheld
- // permissions, erroneously notifying `ScriptInjectionTracker` before sending
- // the injection message to the renderer.
- //
- // TODO(crbug.com/513486355): Once the bug is fixed in
- // `RequestContentScript::InstructRenderProcessToInject()`, rename this test
- // to `WithheldPermissionsDoNotUpdateTracker` and update this expectation to
- // verify that `ScriptInjectionTracker` does not record execution when
- // permissions are withheld:
- // EXPECT_FALSE(
- // ScriptInjectionTracker::DidProcessRunContentScriptFromExtension(
- // *target_process, extension()->id()));
+ // Verify that `ScriptInjectionTracker` does not record that the process ran a
+ // content script from this extension while permissions are withheld.
+ EXPECT_FALSE(ScriptInjectionTracker::DidProcessRunContentScriptFromExtension(
+ *target_process, extension()->id()));
+
+ // Grant permission to run the blocked action on the active tab and verify
+ // that the deferred content script executes and updates the tracker.
+ ASSERT_TRUE(action_runner->WantsToRun(extension()));
+ action_runner->RunForTesting(extension());
+
+ {
+ SCOPED_TRACE("Waiting for content script to execute after granting access");
+ ASSERT_TRUE(script_listener.WaitUntilSatisfied());
+ }
+
EXPECT_TRUE(ScriptInjectionTracker::DidProcessRunContentScriptFromExtension(
*target_process, extension()->id()));
}
diff --git a/chrome/browser/extensions/extension_action_runner_unittest.cc b/chrome/browser/extensions/extension_action_runner_unittest.cc
index ef5798d..5cf2eea 100644
--- a/chrome/browser/extensions/extension_action_runner_unittest.cc
+++ b/chrome/browser/extensions/extension_action_runner_unittest.cc
@@ -585,4 +585,34 @@
EXPECT_FALSE(runner()->WantsToRun(extension));
}
+// Tests that `ExtensionActionRunner::RequiresUserConsentForScriptInjection()`
+// gates `mojom::InjectionType::kDeclarativeScript` injections (from the
+// `RequestContentScript` action of the `chrome.declarativeContent` API) on the
+// extension's host permissions (`PermissionsData::GetPageAccess()`), the same
+// as `mojom::InjectionType::kProgrammaticScript`, rather than on its manifest
+// content script matches (`PermissionsData::GetContentScriptAccess()`).
+TEST_F(ExtensionActionRunnerUnitTest, DeclarativeScriptsUseHostPermissions) {
+ // Add an extension with withheld all hosts permissions and no manifest
+ // content scripts, then navigate to a page it has requested host permissions
+ // for.
+ const Extension* extension = AddExtension();
+ ASSERT_TRUE(extension);
+ NavigateAndCommit(GURL("https://www.google.com"));
+
+ // Content script injections are denied since the extension has no manifest
+ // content scripts matching the page.
+ EXPECT_EQ(PermissionsData::PageAccess::kDenied,
+ runner()->RequiresUserConsentForScriptInjectionForTesting(
+ extension, mojom::InjectionType::kContentScript));
+
+ // Declarative and programmatic script injections are withheld because they
+ // use the extension's withheld host permissions.
+ EXPECT_EQ(PermissionsData::PageAccess::kWithheld,
+ runner()->RequiresUserConsentForScriptInjectionForTesting(
+ extension, mojom::InjectionType::kDeclarativeScript));
+ EXPECT_EQ(PermissionsData::PageAccess::kWithheld,
+ runner()->RequiresUserConsentForScriptInjectionForTesting(
+ extension, mojom::InjectionType::kProgrammaticScript));
+}
+
} // namespace extensions
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page