Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Extensions
DescriptionInsufficient validation of untrusted input in Extensions
ComponentExtensions
Bug ClassLogic Error
Tracker497025738
Fix commit3995e7234978 (chromium/src) +240/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
for
chrome/browser/extensions/api/declarative_content/declarative_content_css_condition_tracker.cc
modified
if
chrome/browser/extensions/extension_security_exploit_browsertest.cc
modified

Files Changed

  • chrome/browser/extensions/api/declarative_content/content_action.cc
  • chrome/browser/extensions/api/declarative_content/declarative_content_css_condition_tracker.cc
  • chrome/browser/extensions/extension_security_exploit_browsertest.cc
From 3995e723497838c70fbd0a6447ee4501a0f745d0 Mon Sep 17 00:00:00 2001
From: Justin Lulejian <[email protected]>
Date: Tue, 14 Apr 2026 17:24:24 -0700
Subject: [PATCH] [Extensions] Harden Declarative Content API against spoofed IPCs

Before this commit, a compromised renderer could spoof the
WatchedPageChange IPC message to report that a CSS selector matched on a
page. This could be used to trick ScriptInjectionTracker into granting
trust for unauthorized pages, leading to unpermitted script injection.
The issue is two-part:

1) The WatchedPageChange IPC could be for any selector and will be
considered valid.

2) Even if the WatchedPageChange IPC was an existing selector, we still
don't check if the extension has host permission before script
injection.

After this commit we address both parts:

For the WatchedPageChange IPC: we add a coarse IPC validation check in
DeclarativeContentCssConditionTracker::OnWatchedPageChanged to ensure
reported selectors are at least known globally. If not, we ignore the
unknown selector so it doesn't trigger any actions. We ignore instead of
terminate the renderer because of a possible race where the browser
might stop tracking the selector before the IPC arrives.

For the script injection: we add a host permission check before granting
script injection trust in
RequestContentScript::InstructRenderProcessToInject.

Both scenarios/parts are tested in
extension_security_exploit_browsertest.cc.

Bug: 497025738
Change-Id: Ic962c0c0e4dffa5fb6086cd32b4c3b72d235b635
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7748669
Reviewed-by: Andrea Orru <[email protected]>
Commit-Queue: Justin Lulejian <[email protected]>
Auto-Submit: Justin Lulejian <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1614799}
---

diff --git a/chrome/browser/extensions/api/declarative_content/content_action.cc b/chrome/browser/extensions/api/declarative_content/content_action.cc
index b1c32f5..85cd204 100644
--- a/chrome/browser/extensions/api/declarative_content/content_action.cc
+++ b/chrome/browser/extensions/api/declarative_content/content_action.cc
@@ -34,6 +34,7 @@
 #include "extensions/common/mojom/host_id.mojom.h"
 #include "extensions/common/mojom/match_origin_as_fallback.mojom-shared.h"
 #include "extensions/common/mojom/run_location.mojom-shared.h"
+#include "extensions/common/permissions/permissions_data.h"
 #include "third_party/skia/include/core/SkBitmap.h"
 #include "ui/gfx/image/image.h"
 #include "ui/gfx/image/image_skia.h"
@@ -374,6 +375,18 @@
 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).
+  std::string error;
+  if (!extension->permissions_data()->CanAccessPage(
+          contents->GetLastCommittedURL(), ExtensionTabUtil::GetTabId(contents),
+          &error)) {
+    return;
+  }
+
   ScriptInjectionTracker::WillExecuteCode(base::PassKey<RequestContentScript>(),
                                           contents->GetPrimaryMainFrame(),
                                           *extension);
diff --git a/chrome/browser/extensions/api/declarative_content/declarative_content_css_condition_tracker.cc b/chrome/browser/extensions/api/declarative_content/declarative_content_css_condition_tracker.cc
index c5d2230d..2a9b8c2f 100644
--- a/chrome/browser/extensions/api/declarative_content/declarative_content_css_condition_tracker.cc
+++ b/chrome/browser/extensions/api/declarative_content/declarative_content_css_condition_tracker.cc
@@ -14,6 +14,8 @@
 #include "chrome/browser/profiles/profile.h"
 #include "content/public/browser/navigation_handle.h"
 #include "content/public/browser/render_process_host.h"
+#include "content/public/browser/web_contents.h"
+#include "extensions/browser/bad_message.h"
 #include "extensions/browser/renderer_startup_helper.h"
 #include "extensions/buildflags/buildflags.h"
 #include "extensions/common/api/declarative/declarative_constants.h"
@@ -218,7 +220,18 @@
     content::WebContents* contents,
     const std::vector<std::string>& css_selectors) {
   DCHECK(per_web_contents_tracker_.contains(contents));
-  per_web_contents_tracker_[contents]->OnWatchedPageChanged(css_selectors);
+
+  std::vector<std::string> valid_selectors;
+  for (const std::string& selector : css_selectors) {
+    // A compromised renderer might send selectors that were not requested.
+    // We ignore unrecognized selectors to avoid race conditions where a
+    // legitimate message arrives after the browser stopped watching a selector.
+    if (watched_css_selector_predicate_count_.contains(selector)) {
+      valid_selectors.push_back(selector);
+    }
+  }
+
+  per_web_contents_tracker_[contents]->OnWatchedPageChanged(valid_selectors);
 }
 
 bool DeclarativeContentCssConditionTracker::EvaluatePredicate(
diff --git a/chrome/browser/extensions/extension_security_exploit_browsertest.cc b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
index 151756f..85d5859 100644
--- a/chrome/browser/extensions/extension_security_exploit_browsertest.cc
+++ b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
@@ -35,6 +35,7 @@
 #include "extensions/browser/extension_web_contents_observer.h"
 #include "extensions/browser/process_manager.h"
 #include "extensions/browser/renderer_startup_helper.h"
+#include "extensions/browser/script_injection_tracker.h"
 #include "extensions/browser/service_worker/service_worker_host.h"
 #include "extensions/common/constants.h"
 #include "extensions/common/extension_features.h"
@@ -85,6 +86,35 @@
     open_extension_mutator_ = std::move(open_extension_mutator);
   }
 
+  void SimulateWatchedPageChange(
+      const std::vector<std::string>& css_selectors) {
+    GetForwardingInterface()->WatchedPageChange(css_selectors);
+  }
+
+  using WatchedPageChangeMutator =
+      base::RepeatingCallback<void(std::vector<std::string>& css_selectors)>;
+  void SetWatchedPageChangeMutator(
+      WatchedPageChangeMutator watched_page_change_mutator) {
+    watched_page_change_mutator_ = std::move(watched_page_change_mutator);
+  }
+
+  // We cannot call `FlushForTesting()` on the receiver side because
+  // `ExtensionFrameHost` uses `content::RenderFrameHostReceiverSet` which does
+  // not expose `FlushForTesting()`. Instead, we do a Mojo round-trip by calling
+  // a method with a callback (`GetAppInstallState`) to ensure all previous
+  // messages on the pipe have been processed.
+  void FlushForTesting() {
+    base::RunLoop run_loop;
+    GetForwardingInterface()->GetAppInstallState(
+        GURL("http://example.com"),
+        base::BindOnce(
+            [](base::OnceClosure quit_closure, const std::string&) {
+              std::move(quit_closure).Run();
+            },
+            run_loop.QuitClosure()));
+    run_loop.Run();
+  }
+
  private:
   mojom::LocalFrameHost* GetForwardingInterface() override {
     return scoped_swap_impl_.old_impl();
@@ -125,9 +155,22 @@
         std::move(port_host));
   }
 
+  void WatchedPageChange(
+      const std::vector<std::string>& css_selectors) override {
+    std::vector<std::string> mutated_selectors = css_selectors;
+    content::RenderFrameHost* current_target_frame =
+        extension_frame_host_->receivers_for_testing().GetCurrentTargetFrame();
+    if (watched_page_change_mutator_ && frame_ == current_target_frame) {
+      watched_page_change_mutator_.Run(mutated_selectors);
+    }
+
+    GetForwardingInterface()->WatchedPageChange(mutated_selectors);
+  }
+
   const raw_ptr<content::RenderFrameHost> frame_ = nullptr;
   RequestMutator request_mutator_;
   OpenChannelToExtensionMutator open_extension_mutator_;
+  WatchedPageChangeMutator watched_page_change_mutator_;
   const raw_ptr<ExtensionFrameHost> extension_frame_host_ = nullptr;
   const mojo::test::ScopedSwapImplForTesting<mojom::LocalFrameHost>
       scoped_swap_impl_;
@@ -745,4 +788,174 @@
             kill_waiter.Wait());
 }
 
+// Tests that a renderer sending an unrecognized selector (one the browser
+// didn't ask to watch) is ignored and does not trigger actions.
+IN_PROC_BROWSER_TEST_F(ExtensionSecurityExploitBrowserTest,
+                       WatchedPageChange_InvalidSelector_Ignored) {
+  // Install an extension that watches "div.target" and injects a script.
+  TestExtensionDir dir;
+  dir.WriteManifest(R"({
+    "name": "Extension",
+    "version": "1.0",
+    "manifest_version": 2,
+    "permissions": [ "declarativeContent", "http://foo.com/*" ],
+    "background": {"scripts": ["background.js"]}
+  })");
+  dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"(
+    chrome.runtime.onInstalled.addListener(() => {
+      chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
+        chrome.declarativeContent.onPageChanged.addRules([{
+          conditions: [
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/extensions/extension_security_exploit_browsertest.cc b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
index 151756f..85d5859 100644
--- a/chrome/browser/extensions/extension_security_exploit_browsertest.cc
+++ b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
@@ -35,6 +35,7 @@
 #include "extensions/browser/extension_web_contents_observer.h"
 #include "extensions/browser/process_manager.h"
 #include "extensions/browser/renderer_startup_helper.h"
+#include "extensions/browser/script_injection_tracker.h"
 #include "extensions/browser/service_worker/service_worker_host.h"
 #include "extensions/common/constants.h"
 #include "extensions/common/extension_features.h"
@@ -85,6 +86,35 @@
     open_extension_mutator_ = std::move(open_extension_mutator);
   }
 
+  void SimulateWatchedPageChange(
+      const std::vector<std::string>& css_selectors) {
+    GetForwardingInterface()->WatchedPageChange(css_selectors);
+  }
+
+  using WatchedPageChangeMutator =
+      base::RepeatingCallback<void(std::vector<std::string>& css_selectors)>;
+  void SetWatchedPageChangeMutator(
+      WatchedPageChangeMutator watched_page_change_mutator) {
+    watched_page_change_mutator_ = std::move(watched_page_change_mutator);
+  }
+
+  // We cannot call `FlushForTesting()` on the receiver side because
+  // `ExtensionFrameHost` uses `content::RenderFrameHostReceiverSet` which does
+  // not expose `FlushForTesting()`. Instead, we do a Mojo round-trip by calling
+  // a method with a callback (`GetAppInstallState`) to ensure all previous
+  // messages on the pipe have been processed.
+  void FlushForTesting() {
+    base::RunLoop run_loop;
+    GetForwardingInterface()->GetAppInstallState(
+        GURL("http://example.com"),
+        base::BindOnce(
+            [](base::OnceClosure quit_closure, const std::string&) {
+              std::move(quit_closure).Run();
+            },
+            run_loop.QuitClosure()));
+    run_loop.Run();
+  }
+
  private:
   mojom::LocalFrameHost* GetForwardingInterface() override {
     return scoped_swap_impl_.old_impl();
@@ -125,9 +155,22 @@
         std::move(port_host));
   }
 
+  void WatchedPageChange(
+      const std::vector<std::string>& css_selectors) override {
+    std::vector<std::string> mutated_selectors = css_selectors;
+    content::RenderFrameHost* current_target_frame =
+        extension_frame_host_->receivers_for_testing().GetCurrentTargetFrame();
+    if (watched_page_change_mutator_ && frame_ == current_target_frame) {
+      watched_page_change_mutator_.Run(mutated_selectors);
+    }
+
+    GetForwardingInterface()->WatchedPageChange(mutated_selectors);
+  }
+
   const raw_ptr<content::RenderFrameHost> frame_ = nullptr;
   RequestMutator request_mutator_;
   OpenChannelToExtensionMutator open_extension_mutator_;
+  WatchedPageChangeMutator watched_page_change_mutator_;
   const raw_ptr<ExtensionFrameHost> extension_frame_host_ = nullptr;
   const mojo::test::ScopedSwapImplForTesting<mojom::LocalFrameHost>
       scoped_swap_impl_;
@@ -745,4 +788,174 @@
             kill_waiter.Wait());
 }
 
+// Tests that a renderer sending an unrecognized selector (one the browser
+// didn't ask to watch) is ignored and does not trigger actions.
+IN_PROC_BROWSER_TEST_F(ExtensionSecurityExploitBrowserTest,
+                       WatchedPageChange_InvalidSelector_Ignored) {
+  // Install an extension that watches "div.target" and injects a script.
+  TestExtensionDir dir;
+  dir.WriteManifest(R"({
+    "name": "Extension",
+    "version": "1.0",
+    "manifest_version": 2,
+    "permissions": [ "declarativeContent", "http://foo.com/*" ],
+    "background": {"scripts": ["background.js"]}
+  })");
+  dir.WriteFile(FILE_PATH_LITERAL("background.js"), R"(
+    chrome.runtime.onInstalled.addListener(() => {
+      chrome.declarativeContent.onPageChanged.removeRules(undefined, () => {
+        chrome.declarativeContent.onPageChanged.addRules([{
+          conditions: [
+            new chrome.declarativeContent.PageStateMatcher({
+              css: ["div.target"]
+            })
+          ],
+          actions: [
+            new chrome.declarativeContent.RequestContentScript({
+              js: ["script.js"]
+            })
+          ]
+        }]);
+      });
+    });
+  )");
+  dir.WriteFile(FILE_PATH_LITERAL("script.js"), "console.log('injected');");
+
+  const Extension* extension = LoadExtension(dir.UnpackedPath());
+  ASSERT_TRUE(extension);
+
+  // Navigate to a test page on foo.com where the extension has permission.
+  GURL test_page_url =
+      embedded_test_server()->GetURL("foo.com", "/title1.html");
+  auto* web_contents = GetActiveWebContents();
+  ASSERT_TRUE(NavigateToURL(web_contents, test_page_url));
+  content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
+
+  // Prepare to intercept WatchedPageChange.
+  auto interceptor =
+      std::make_unique<ExtensionFrameHostInterceptor>(main_frame);
+
+  // Simulate a spoofed IPC with a selector the browser didn't ask to watch.
+  std::vector<std::string> spoofed_selectors = {"div.spoofed"};
+  interceptor->SimulateWatchedPageChange(spoofed_selectors);
+  interceptor->FlushForTesting();
+
+  // Verify that `ScriptInjectionTracker` doesn't show injection for the
+  // extension.
+  EXPECT_FALSE(ScriptInjectionTracker::DidProcessRunContentScriptFromExtension(
+      *main_frame->GetProcess(), extension->id()));
+
+  // Now simulate a valid selector to prove it *would* work.
+  std::vector<std::string> valid_selectors = {"div.target"};
+  interceptor->SimulateWatchedPageChange(valid_selectors);
+  interceptor->FlushForTesting();
+
+  // Verify that `ScriptInjectionTracker` does show injection now.
+  EXPECT_TRUE(ScriptInjectionTracker::DidProcessRunContentScriptFromExtension(
+      *main_frame->GetProcess(), extension->id()));
+}
+
+// Tests that a renderer cannot trigger content script injection on a page the
+// extension has no permission for, even if it spoofs a valid CSS selector. This
+// is a regression test for crbug.com/497025738 and exercises the host
+// permission check in `RequestContentScript::InstructRenderProcessToInject`.
+//
+// High-level flow:
+// 1. Install Extension A that watches "div.target" on google.com. This makes
+//    the selector globally known to the browser's CSS condition tracker. This
+//    allows a compromised renderer to pass the check we have for valid
+//    selectors on the `extensions::mojom::LocalFrameHost::WatchedPageChange`
+//    IPC.
+// 2. Install Extension B that has a declarative rule to inject a script when
+//    "div.target" matches, but Extension B only has host permission for
+//    bar.com.
+// 3. Navigate to foo.com (where Extension B has no permission).
+// 4. Simulate a compromised renderer sending a
+//    `extensions::mojom::LocalFrameHost::WatchedPageChange` IPC claiming that
+//    "div.target" matched on foo.com.
+// 5. Verify that the browser blocks the script injection of Extension B because
+//    Extension B lacks permission for foo.com, despite the selector being
+//    valid.
+IN_PROC_BROWSER_TEST_F(ExtensionSecurityExploitBrowserTest,
+                       WatchedPageChange_ValidSelector_PermissionDenied) {
+  // 1. Install Extension A to make "div.target" globally known.
+  TestExtensionDir dir_a;
+  dir_a.WriteManifest(R"({
+    "name": "Extension A",
+    "version": "1.0",
+    "manifest_version": 2,
+    "permissions": [ "declarativeContent", "http://google.com/*" ],
+    "background": {"scripts": ["background.js"]}
+  })");
+  dir_a.WriteFile(FILE_PATH_LITERAL("background.js"), R"(
+    var rule = {
+      conditions: [
+        new chrome.declarativeContent.PageStateMatcher({
+          pageUrl: { hostEquals: 'google.com' },
+          css: ['div.target']
+        })
+      ],
+      actions: [ new chrome.declarativeContent.ShowAction() ]
+    };
+    chrome.runtime.onInstalled.addListener(function() {
+      chrome.declarativeContent.onPageChanged.addRules([rule]);
+    });
+  )");
+  const Extension* extension_a = LoadExtension(dir_a.UnpackedPath());
+  ASSERT_TRUE(extension_a);
+
+  // 2. Install Extension B (target). It only has permission for bar.com.
+  TestExtensionDir dir_b;
+  dir_b.WriteManifest(R"({
+    "name": "Extension B",
+    "version": "1.0",
+    "manifest_version": 2,
+    "permissions": [ "declarativeContent", "http://bar.com/*" ],
+    "background": {"scripts": ["background.js"]}
+  })");
+  dir_b.WriteFile(FILE_PATH_LITERAL("background.js"), R"(
+    var rule = {
+      conditions: [
+        new chrome.declarativeContent.PageStateMatcher({
+          css: ['div.target']
+        })
+      ],
+      actions: [ new chrome.declarativeContent.RequestContentScript({
+        js: ["content.js"]
+      }) ]
+    };
+    chrome.runtime.onInstalled.addListener(function() {
+      chrome.declarativeContent.onPageChanged.addRules([rule]);
+    });
+  )");
+  dir_b.WriteFile(FILE_PATH_LITERAL("content.js"), R"(
+    chrome.test.sendMessage('injected');
+  )");
+  const Extension* extension_b = LoadExtension(dir_b.UnpackedPath());
+  ASSERT_TRUE(extension_b);
+
+  // Navigate to foo.com (where Extension B has no permission).
+  GURL test_page_url =
+      embedded_test_server()->GetURL("foo.com", "/title1.html");
+  auto* web_contents = GetActiveWebContents();
+  ASSERT_TRUE(NavigateToURL(web_contents, test_page_url));
+  content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
+
+  // Prepare to intercept WatchedPageChange.
+  auto interceptor =
+      std::make_unique<ExtensionFrameHostInterceptor>(main_frame);
+
+  // Simulate a spoofed IPC with a selector that is valid globally, bypassing
+  // the IPC check.
+  std::vector<std::string> spoofed_selectors = {"div.target"};
+  interceptor->SimulateWatchedPageChange(spoofed_selectors);
+  interceptor->FlushForTesting();
+
+  // Verify that `ScriptInjectionTracker` doesn't show injection for Extension B
+  // on foo.com because we've checked that the extension has host permission
+  // before allowing the script injection.
+  EXPECT_FALSE(ScriptInjectionTracker::DidProcessRunContentScriptFromExtension(
+      *main_frame->GetProcess(), extension_b->id()));
+}
+
 }  // namespace extensions
Loading diff…

Original Bug Report

reported by [email protected]

Privilege escalation via spoofed WatchedPageChange IPC in declarativeContent

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A compromised renderer can spoof the WatchedPageChange IPC to trick the browser into matching a declarativeContent rule with a RequestContentScript action. This unconditionally grants the renderer permanent content script privileges for that extension, allowing it to bypass security checks and communicate with the extension’s background page.

Affected files:

  • chrome/browser/extensions/api/declarative_content/content_action.cc
  • chrome/browser/extensions/chrome_extension_frame_host.cc
  • chrome/browser/extensions/api/declarative_content/declarative_content_css_condition_tracker.cc
  • extensions/browser/script_injection_tracker.cc
  • chrome/browser/extensions/api/declarative_content/chrome_content_rules_registry.cc
  • extensions/browser/api/messaging/message_service_bindings.cc

Estimated timestamp from git blame: 2023-10-19

Description

A logic flaw exists in the handling of the declarativeContent extension API, specifically concerning the experimental RequestContentScript action.

When a renderer sends an extensions::mojom::LocalFrameHost::WatchedPageChange IPC message containing CSS selectors, the browser’s DeclarativeContentCssConditionTracker stores these selectors verbatim and re-evaluates declarative rules. If an installed extension uses a PageStateMatcher that depends only on these CSS selectors, the browser will trigger the associated RequestContentScript action.

The vulnerability occurs in RequestContentScript::InstructRenderProcessToInject. This function unconditionally calls ScriptInjectionTracker::WillExecuteCode, which permanently adds the extension’s ID to the renderer process’s content_scripts_ allowed list and pushes an isolated world URLLoaderFactory. This happens without verifying if the extension actually has host permissions to run on the attacker-controlled page.

Once added to the content_scripts_ tracker, the compromised renderer can spoof messaging IPCs (like OpenChannelToExtension) claiming to be the extension’s content script. The browser’s message service bindings will check ScriptInjectionTracker::DidProcessRunContentScriptFromExtension, which will now return true, bypassing the bad_message::EMF_INVALID_EXTENSION_ID_FOR_CONTENT_SCRIPT check. The renderer can then attack the extension’s privileged background page.

Note: RequestContentScript is currently an experimental API, which limits the real-world impact to extensions specifically utilizing this feature on supported channels.

Potential Attack Steps

Note: These are suggested steps based on code analysis; our tooling agent does not currently have the ability to run a live proof-of-concept.

  1. An attacker compromises a main-frame renderer process.
  2. The attacker identifies an installed extension using declarativeContent.RequestContentScript triggered by a CSS-only PageStateMatcher.
  3. The compromised renderer sends a spoofed extensions::mojom::LocalFrameHost::WatchedPageChange IPC to the browser, providing the exact CSS selectors the target extension is looking for.
  4. The browser blindly accepts these selectors and triggers the RequestContentScript action.
  5. RequestContentScript::InstructRenderProcessToInject unconditionally calls ScriptInjectionTracker::WillExecuteCode, permanently granting the renderer process content-script-level trust for that extension.
  6. The compromised renderer sends extensions::mojom::LocalFrameHost::OpenChannelToExtension, setting the source endpoint type to kContentScript and the extension ID to the target extension.
  7. The browser validates the request against the newly poisoned ScriptInjectionTracker, allows the connection, and the attacker gains full messaging access to the extension’s background page.

Suggested Fix

  1. Permission Check Before Trust Grant: In RequestContentScript::InstructRenderProcessToInject (within chrome/browser/extensions/api/declarative_content/content_action.cc), add a permissions check to verify that the extension is actually authorized to inject a script into the given WebContents (e.g., checking host permissions) before calling ScriptInjectionTracker::WillExecuteCode.
  2. IPC Validation: Consider adding validation to ChromeExtensionFrameHost::WatchedPageChange to ensure the renderer is only reporting on selectors the browser actually requested it to watch.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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