CVE-2026-13916
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forios/web/web_state/ui/wk_content_rule_list_util.mm |
modified |
Files Changed
ios/web/web_state/ui/wk_content_rule_list_util.mmios/web/web_state/ui/wk_content_rule_list_util_unittest.mm
Patch
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"]);
Regression Test / PoC
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"]);
Original Bug Report
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.
- Identify a target victim website (e.g.,
https://victim.com/sensitive-action) that relies on JavaScript for frame-busting or UI security enforcement. - Create a malicious webpage at
https://attacker.com/exploit.htmlthat embeds the victim’s site in an iframe:<iframe src="https://victim.com/sensitive-action"></iframe>. - 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://. - When the browser navigates to the attacker’s URL, the
if-top-urlcondition for the local-blocking rule evaluates to true becausefile://is present in the query string. - The iframe document loads successfully (as
documentis excluded from the block list), but WebKit silently aborts all subsequent requests for the victim’s scripts and stylesheets. - 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.