Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Chrome for iOS
DescriptionInappropriate implementation in Chrome for iOS
ComponentChrome for iOS
Bug ClassLogic Error
Tracker508283108
Fix commitc8fe8b39d9ca (chromium/src) +16/-12
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
for
ios/web/web_state/ui/wk_content_rule_list_util.mm
modified

Files Changed

  • ios/web/web_state/ui/wk_content_rule_list_util.mm
  • ios/web/web_state/ui/wk_content_rule_list_util_unittest.mm
From c8fe8b39d9ca680e9055e80358a2166ed54cd3fe Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Wed, 06 May 2026 05:30:38 -0700
Subject: [PATCH] [iOS] Anchor WKContentRuleList regular expressions

Anchor regular expressions in WKContentRuleList triggers with '^' to
ensure they only match from the start of the URL. This prevents
triggering blocking rules via query strings or fragments in malicious
URLs (e.g., https://example.com/?file://).

The trailing .* is retained despite being redundant in a prefix match.
This is done to maintain clarity of intent (explicitly signaling that
the rule matches the entire URL) and to provide robustness against
potential implementation-specific behaviors in different versions of the
WebKit content blocker regex engine.

The official Apple documentation for WKContentRuleList regular
expression syntax is somewhat vague [1], but the underlying engine
(WebKit) treats these as unanchored by default, requiring explicit start
anchors for prefix matching, as evidenced by its unit tests [2].

In theory, anchoring these patterns also makes them more performant.

[1]
https://developer.apple.com/documentation/safariservices/creating-a-content-blocker
[2]
https://github.com/WebKit/WebKit/blob/e1cc09104fdd8f98c6985e30a5027785dff163ff/Tools/TestWebKitAPI/Tests/WebCore/ContentExtensions.cpp#L817-818

Fixed: 508283108
Change-Id: I9df545167629f31659287ac2da9d7ab054c82720
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7816358
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Reviewed-by: Gauthier Ambard <gambard@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1626075}
---

diff --git a/ios/web/web_state/ui/wk_content_rule_list_util.mm b/ios/web/web_state/ui/wk_content_rule_list_util.mm
index ba0b6c6..af37bd0 100644
--- a/ios/web/web_state/ui/wk_content_rule_list_util.mm
+++ b/ios/web/web_state/ui/wk_content_rule_list_util.mm
@@ -11,17 +11,18 @@
 namespace web {
 
 NSString* CreateLocalBlockingJsonRuleList() {
-  NSMutableArray* local_schemes_urls = [@[ @"file://.*" ] mutableCopy];
+  NSMutableArray* local_schemes_urls = [@[ @"^file://.*" ] mutableCopy];
   WebClient::Schemes schemes;
   GetWebClient()->AddAdditionalSchemes(&schemes);
   GetWebClient()->GetAdditionalWebUISchemes(&(schemes.standard_schemes));
   for (std::string scheme : schemes.standard_schemes) {
-    [local_schemes_urls addObject:base::SysUTF8ToNSString(scheme + "://.*")];
+    [local_schemes_urls
+        addObject:base::SysUTF8ToNSString("^" + scheme + "://.*")];
   }
 
   NSDictionary* local_block = @{
     @"trigger" : @{
-      @"url-filter" : @"https?://.*",
+      @"url-filter" : @"^https?://.*",
       @"if-top-url" : local_schemes_urls,
       @"resource-type" : @[
         // These should be all resource types except document.
@@ -38,8 +39,8 @@
 
   NSDictionary* allow_crbug = @{
     @"trigger" : @{
-      @"url-filter" : @"https://bugs\\.chromium\\.org/.*",
-      @"if-top-url" : @[ @"chrome://.*" ],
+      @"url-filter" : @"^https://bugs\\.chromium\\.org/.*",
+      @"if-top-url" : @[ @"^chrome://.*" ],
       @"resource-type" : @[
         // Allow opening crbug from chrome:// urls
         @"popup"
@@ -62,8 +63,8 @@
 NSString* CreateMixedContentAutoUpgradeJsonRuleList() {
   NSDictionary* mixed_content_autoupgrade = @{
     @"trigger" : @{
-      @"url-filter" : @"http://.*",
-      @"if-top-url" : @[ @"https://.*" ],
+      @"url-filter" : @"^http://.*",
+      @"if-top-url" : @[ @"^https://.*" ],
       @"resource-type" : @[
         // Only upgrade image and media (i.e. audio and video) per
         // https://www.w3.org/TR/mixed-content/#upgrade-algorithm
diff --git a/ios/web/web_state/ui/wk_content_rule_list_util_unittest.mm b/ios/web/web_state/ui/wk_content_rule_list_util_unittest.mm
index cdff317..551e1b64d 100644
--- a/ios/web/web_state/ui/wk_content_rule_list_util_unittest.mm
+++ b/ios/web/web_state/ui/wk_content_rule_list_util_unittest.mm
@@ -31,11 +31,14 @@
   id block_rule = json[0];
   ASSERT_TRUE([block_rule isKindOfClass:[NSDictionary class]]);
   NSArray* filtered_schemes = @[
-    @"file://.*", [@(kTestWebUIScheme) stringByAppendingString:@"://.*"],
-    [@(kTestAppSpecificScheme) stringByAppendingString:@"://.*"]
+    @"^file://.*",
+    [@"^" stringByAppendingString:[@(kTestWebUIScheme)
+                                      stringByAppendingString:@"://.*"]],
+    [@"^" stringByAppendingString:[@(kTestAppSpecificScheme)
+                                      stringByAppendingString:@"://.*"]]
   ];
   ASSERT_NSEQ(filtered_schemes, block_rule[@"trigger"][@"if-top-url"]);
-  ASSERT_NSEQ(@"https?://.*", block_rule[@"trigger"][@"url-filter"]);
+  ASSERT_NSEQ(@"^https?://.*", block_rule[@"trigger"][@"url-filter"]);
   NSArray* filtered_types = @[
     @"image", @"style-sheet", @"script", @"font", @"raw", @"svg-document",
     @"media", @"popup", @"ping"
@@ -58,10 +61,10 @@
   ASSERT_TRUE([json isKindOfClass:[NSArray class]]);
   id block_rule = json[0];
   ASSERT_TRUE([block_rule isKindOfClass:[NSDictionary class]]);
-  NSArray* filtered_schemes = @[ @"https://.*" ];
+  NSArray* filtered_schemes = @[ @"^https://.*" ];
   ASSERT_NSEQ(filtered_schemes, block_rule[@"trigger"][@"if-top-url"]);
 
-  ASSERT_NSEQ(@"http://.*", block_rule[@"trigger"][@"url-filter"]);
+  ASSERT_NSEQ(@"^http://.*", block_rule[@"trigger"][@"url-filter"]);
   NSArray* filtered_types = @[ @"image", @"media" ];
   ASSERT_NSEQ(filtered_types, block_rule[@"trigger"][@"resource-type"]);
   ASSERT_NSEQ(@"make-https", block_rule[@"action"][@"type"]);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/ios/web/web_state/ui/wk_content_rule_list_util_unittest.mm b/ios/web/web_state/ui/wk_content_rule_list_util_unittest.mm
index cdff317..551e1b64d 100644
--- a/ios/web/web_state/ui/wk_content_rule_list_util_unittest.mm
+++ b/ios/web/web_state/ui/wk_content_rule_list_util_unittest.mm
@@ -31,11 +31,14 @@
   id block_rule = json[0];
   ASSERT_TRUE([block_rule isKindOfClass:[NSDictionary class]]);
   NSArray* filtered_schemes = @[
-    @"file://.*", [@(kTestWebUIScheme) stringByAppendingString:@"://.*"],
-    [@(kTestAppSpecificScheme) stringByAppendingString:@"://.*"]
+    @"^file://.*",
+    [@"^" stringByAppendingString:[@(kTestWebUIScheme)
+                                      stringByAppendingString:@"://.*"]],
+    [@"^" stringByAppendingString:[@(kTestAppSpecificScheme)
+                                      stringByAppendingString:@"://.*"]]
   ];
   ASSERT_NSEQ(filtered_schemes, block_rule[@"trigger"][@"if-top-url"]);
-  ASSERT_NSEQ(@"https?://.*", block_rule[@"trigger"][@"url-filter"]);
+  ASSERT_NSEQ(@"^https?://.*", block_rule[@"trigger"][@"url-filter"]);
   NSArray* filtered_types = @[
     @"image", @"style-sheet", @"script", @"font", @"raw", @"svg-document",
     @"media", @"popup", @"ping"
@@ -58,10 +61,10 @@
   ASSERT_TRUE([json isKindOfClass:[NSArray class]]);
   id block_rule = json[0];
   ASSERT_TRUE([block_rule isKindOfClass:[NSDictionary class]]);
-  NSArray* filtered_schemes = @[ @"https://.*" ];
+  NSArray* filtered_schemes = @[ @"^https://.*" ];
   ASSERT_NSEQ(filtered_schemes, block_rule[@"trigger"][@"if-top-url"]);
 
-  ASSERT_NSEQ(@"http://.*", block_rule[@"trigger"][@"url-filter"]);
+  ASSERT_NSEQ(@"^http://.*", block_rule[@"trigger"][@"url-filter"]);
   NSArray* filtered_types = @[ @"image", @"media" ];
   ASSERT_NSEQ(filtered_types, block_rule[@"trigger"][@"resource-type"]);
   ASSERT_NSEQ(@"make-https", block_rule[@"action"][@"type"]);
Loading diff…

Original Bug Report

reported by li...@chromium.org

Potential UI Redressing via Unanchored if-top-url Regex in iOS Content Blocker

Flapjack, 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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: The iOS Chrome content blocker uses unanchored regular expressions (e.g., file://.*) for its if-top-url trigger to block external subresources on privileged pages. An attacker can potentially exploit this by appending ?file:// to their own URL, tricking the browser into globally blocking JavaScript and CSS on cross-origin iframes. This allows bypassing legacy frame-busting scripts to facilitate clickjacking attacks.

Affected files:

  • ios/web/web_state/ui/wk_content_rule_list_util.mm

Estimated timestamp from git blame: 2021-01-27

Description

In ios/web/web_state/ui/wk_content_rule_list_util.mm, the function CreateLocalBlockingJsonRuleList() generates JSON rules for Apple’s WKContentRuleList to prevent local or privileged schemes from loading remote HTTP/HTTPS subresources.

The if-top-url trigger condition is populated with an array of regular expressions representing these local schemes, such as file://.* and chrome://.*. However, in WebKit Content Blocker semantics, regular expressions are unanchored by default. Because these strings lack the ^ start anchor, WebKit interprets them as matching any top-level URL that contains the substring file:// or chrome:// anywhere within it.

Crucially, the rule’s resource-type array explicitly excludes document (to avoid breaking error pages). This means that if an attacker hosts a malicious page with ?file:// in the URL and frames a victim site, the victim iframe document itself will load, but all of its subsequent script, style-sheet, and image requests will match the rule’s https?://.* filter and be silently blocked by the browser.

Impact

This vulnerability provides an attacker with a universal primitive to selectively disable JavaScript and CSS on any cross-origin site framed on their page. This can be weaponized to reliably bypass legacy JS-based frame-busting logic, intentionally break site layouts, or perform UI redressing/clickjacking attacks against authenticated sessions on target websites that do not utilize modern X-Frame-Options or CSP: frame-ancestors protections.

Potential Reproduction Steps

Note: The following are suggested potential steps to trigger the vulnerability. Our tooling agent does not yet have the ability to run code to produce a working proof of concept.

  1. Identify a target victim website (e.g., https://victim.com/sensitive-action) that relies on JavaScript for frame-busting or UI security enforcement.
  2. Create a malicious webpage at https://attacker.com/exploit.html that embeds the victim’s site in an iframe: <iframe src="https://victim.com/sensitive-action"></iframe>.
  3. Entice a victim using iOS Chrome to visit the exploit page with a privileged scheme string appended to the URL, such as: https://attacker.com/exploit.html?file://.
  4. When the browser navigates to the attacker’s URL, the if-top-url condition for the local-blocking rule evaluates to true because file:// is present in the query string.
  5. The iframe document loads successfully (as document is excluded from the block list), but WebKit silently aborts all subsequent requests for the victim’s scripts and stylesheets.
  6. The attacker can now overlay transparent elements over the neutered victim iframe to perform clickjacking.

Suggested Fix

Explicitly anchor the regular expressions generated in CreateLocalBlockingJsonRuleList() with the ^ character to ensure they only match when the URL starts with the specified scheme.

// In ios/web/web_state/ui/wk_content_rule_list_util.mm
NSMutableArray* local_schemes_urls = [@[ @"^file://.*" ] mutableCopy];
// ...
for (std::string scheme : schemes.standard_schemes) {
  [local_schemes_urls addObject:base::SysUTF8ToNSString("^" + scheme + "://.*")];
}

Evaluated with Chrome root at commit: cc901875d53bf4e4fe0e01f02843871da4106e70


Results 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