← WebKit Silent-Fix Report — 2026-W34

15fa22b107d107d2c48bb99373ac9767dcf1bf62  Extension scripts/style sheets are injected into denied domains if Other Websites are set to allow.

severity medium class Bypass confidence 0.90 WebExtension injected content match patterns exploitable-grade
Timothy Hatcher Wed Aug 19 15:36:05 2026 -0700 full: 15fa22b107d107d2c48bb99373ac9767dcf1bf62 view on GitHub ↗
Primitive: Extension content injected into denied domains despite exclude patterns
Triage note: Exclude match patterns were not correctly merged (unionWith replaced with addAll) so base exclude patterns were dropped, letting scripts/stylesheets inject into user-denied domains.
Contents

The bug at a glance

The bug lets a WebExtension inject its content scripts and style sheets into domains the user (or policy) explicitly denied, defeating the per-site permission model that is the extension security boundary. Impact is scoped to the extension’s own capabilities on pages it was supposed to be barred from — content-script access to page DOM/DOM-based data on a denied origin — rather than direct browser memory corruption, and it requires an installed extension whose match patterns are broad (e.g. :///*) with a narrower deny. That reachability profile and the injected-content (not privileged-API) blast radius put it at Medium/6.1.

WebExtension content injection is filtered by two sets of match patterns: include patterns say where to inject and exclude patterns say where never to inject, with a base set of exclude patterns representing user-/policy-denied domains that must always be honored. During an Objective-C++ to C++ port, the in-place set union NSMutableSet -unionSet: was mistranslated to WTF::HashSet::unionWith(), which in WebKit returns a new set and does not modify the receiver. So the base (denied-domain) exclude patterns were computed and then thrown away, leaving excludeMatchPatternsSet containing only the extension’s own declared exclude patterns. An extension granted all hosts (allHostsAndSchemesMatchPattern) but with a specific origin denied would still inject scripts and CSS into that denied origin. The one-word fix restores addAll(), which merges in place.

Root cause

The vulnerable state is the exclude-pattern set built in WebExtensionContext::addInjectedContent for each piece of injected content. The set is seeded with the content’s own expandedExcludeMatchPatternStrings() via addAll(), and then the base exclude patterns (baseExcludeMatchPatternsSet) — which encode the domains the user or administrator denied — are supposed to be merged in so that injection is suppressed there regardless of the include patterns.

The reaching path is normal extension loading: whenever addInjectedContent runs (content_scripts from the manifest, or scripting API registrations), it assembles excludeMatchPatterns from excludeMatchPatternsSet and hands them to the user content controller as the never-inject filter. If baseExcludeMatchPatternsSet is dropped, denied origins are simply absent from the filter.

The defect is that excludeMatchPatternsSet.unionWith(baseExcludeMatchPatternsSet) does not mutate excludeMatchPatternsSet — WTF::HashSet::unionWith returns a fresh unioned set that is discarded — whereas the original Objective-C code used NSMutableSet -unionSet:, which mutates the receiver in place. The result: baseExcludeMatchPatternsSet (the denied-domain patterns) never made it into the exclude list actually used, so an extension with broad include patterns (e.g. matches :///*) injected into origins the user explicitly denied.

The fix changes the call to excludeMatchPatternsSet.addAll(baseExcludeMatchPatternsSet), which appends all elements of the base set into excludeMatchPatternsSet in place, matching the original -unionSet: semantics. The subsequent copyToVector(excludeMatchPatternsSet) then carries the denied-domain patterns, and injection into denied domains is correctly suppressed.

Key code

WebExtensionContext.cpp: in-place addAll() restores the dropped base exclude patterns

HashSet<String> excludeMatchPatternsSet;
excludeMatchPatternsSet.addAll(injectedContentData.expandedExcludeMatchPatternStrings());
-excludeMatchPatternsSet.unionWith(baseExcludeMatchPatternsSet);
+excludeMatchPatternsSet.addAll(baseExcludeMatchPatternsSet);

auto excludeMatchPatterns = copyToVector(excludeMatchPatternsSet);

Patch walkthrough

  • Source/WebKit/UIProcess/Extensions/WebExtensionContext.cpp — In addInjectedContent, the line merging the base (denied-domain) exclude patterns into the per-content exclude set is changed from excludeMatchPatternsSet.unionWith(baseExcludeMatchPatternsSet) to excludeMatchPatternsSet.addAll(baseExcludeMatchPatternsSet). unionWith returns a new set and leaves the receiver unchanged, so the base patterns were silently dropped; addAll inserts them in place, restoring the original NSMutableSet -unionSet: behavior so copyToVector then includes the denied domains in the exclude filter.
  • Tools/TestWebKitAPI/Tests/WebKit/WKWebView/WKWebExtensionAPIScripting.mm — Adds TEST(WKWebExtensionAPIScripting, ContentScriptsRespectDeniedMatchPatterns). It loads an extension whose content_scripts match :///* injecting content.js (sets document.body.dataset.scriptInjected) and content.css (red background), grants allHostsAndSchemesMatchPattern, then denies localhost specifically. Test 1 loads localhost and asserts neither the script flag nor the red background is present (injection suppressed on the denied origin); Test 2 loads 127.0.0.1 (not denied) and asserts both ARE present (injection still works where allowed). It also imports TestNavigationDelegate.h for navigation waiting.

Background

Include vs exclude match patterns — A content script is injected into pages matching its include (matches) patterns except those matching its exclude patterns. Exclude patterns are the hard negative filter and are where user/policy denials must be enforced.

baseExcludeMatchPatternsSet — A per-context set of exclude patterns representing domains the user or administrator has denied the extension; it must be merged into every injected content’s exclude list so denials are honored regardless of how broad the include patterns are.

HashSet::unionWith vs addAll (WTF) — WTF::HashSet::unionWith() returns a new set containing the union and leaves the receiver unchanged; HashSet::addAll() inserts the argument’s elements into the receiver in place. NSMutableSet -unionSet: is the in-place operation, so the faithful port is addAll().

ObjC++ to C++ port hazard — This regression came from mechanically translating Cocoa collection APIs to WTF containers; -unionSet: (mutating) and -setByAddingObjectsFromSet:/unionWith (non-mutating) are easy to conflate, silently dropping data.

Vulnerability window

  1. Original ObjC++ code — addInjectedContent used NSMutableSet -unionSet: to merge base (denied) exclude patterns into the content’s exclude set in place.
  2. Port regression — During ObjC++-to-C++ conversion the call became HashSet::unionWith(), which returns a new set and does not mutate the receiver, so the denied-domain patterns were discarded.
  3. Latent bypass — Extensions with broad include patterns but specific denies had their exclude filter built without the denied domains, so scripts/CSS injected everywhere the include patterns matched.
  4. Discovery — Reported as webkit.org/b/309516 / rdar://171724038: extension content injected into denied domains when Other Websites is set to allow.
  5. Fix — unionWith() changed to addAll() to merge in place, and a regression test (ContentScriptsRespectDeniedMatchPatterns) added.

Triggering

Reconstructable from the added API test. Load a manifest v3 extension with content_scripts matching :///* that injects a content.js (writes document.body.dataset.scriptInjected=‘true’) and content.css (body background red !important). Grant WKWebExtensionMatchPattern.allHostsAndSchemesMatchPattern, then set permission status Denied for a specific origin (e.g. http://localhost). Navigate to that denied origin: pre-fix, document.body.dataset.scriptInjected reads ’true’ and the computed background is rgb(255,0,0), proving the script and stylesheet were injected into the denied domain; post-fix both are absent. Navigating to a non-denied loopback (127.0.0.1) shows injection still occurs, confirming only denied domains are protected. No memory-corruption primitive is involved.

Exploitation

  1. Premise — A user installs an extension with broad host match patterns (e.g. :///*) and then denies it on a sensitive origin, expecting no injection there.
  2. Bypass — Because the denied-domain exclude patterns were dropped, the extension’s content scripts and style sheets run on the denied origin, giving the extension DOM access (reading page contents, form data, or altering appearance) exactly where it was supposed to be blocked.
  3. Scope — The gain is the extension’s normal content-script capability on an origin it should not touch — a permission/policy bypass, not a privilege escalation beyond what content scripts can do; it depends on a mismatch between broad include grants and narrower denies.

Detection & hunting

For defenders and SOC / detection engineers:

  • Injection on denied origin — On a page for an explicitly denied origin, observe whether an extension’s known content-script side effects (injected DOM markers, style sheets) appear; presence indicates the bypass.
  • Exclude-vector contents — Instrument addInjectedContent to log the final excludeMatchPatterns vector and confirm baseExcludeMatchPatternsSet entries (denied domains) are present.
  • Static scan — Grep for HashSet::unionWith() whose return value is unused — a strong signal of a mistranslated in-place NSMutableSet -unionSet:.

Audit directions

  • ObjC++-to-C++ set/collection ports — Audit all conversions of NSMutableSet -unionSet:/-minusSet:/-intersectSet: to WTF containers, checking that in-place mutation was preserved (addAll/removeAll) and non-mutating results are not discarded.
  • Other injected-content filters — Review every place WebExtensionContext assembles include/exclude match patterns (content scripts, scripting.registerContentScripts, user style sheets) for correct merging of base denied patterns.
  • Permission-boundary enforcement — Verify that denied/blocked match patterns are enforced at the point of injection for all injection types, not only at grant time, so a later broad grant cannot re-expose a denied origin.

Before / after

Loading diff…