CVE-2026-5898
Overview
Background
- `net::GURLWithNSURL`
- The Apple-platform helper that converts a Foundation
NSURL(as surfaced by WebKit/WKWebViewon iOS) into Chromium’sGURL. Its output feeds navigation state such as the web state’s last committed URL, which the omnibox displays. - `absoluteString` vs `dataRepresentation`
- Two ways to read an
NSURL. Per the patch comment,absoluteString(and every accessor other thandataRepresentation) can re-encode the URL, for example turningabout:blank#hashintoabout:blank%23hash, whiledataRepresentationreturns the original bytes. - Omnibox / location view on iOS
- The URL bar that shows the committed URL of the current page. It is the primary security indicator telling the user which document they are looking at.
- `kUseNSURLDataForGURLConversion`
- A new Apple-only
base::Feature, enabled by default, that gates the corrected conversion logic. It is also added to the app-shim’s feature allow-list inapp_shim_controller.mm.
Root Cause Analysis
Before the patch, net::GURLWithNSURL always built the GURL from url.absoluteString.UTF8String. For about: URLs with a fragment, Foundation percent-encodes the #, so about:blank#hash became GURL("about:blank%23hash"). The removed iOS EarlGrey test comments document the consequence for the window.open / location.replace scenario (webScenarioLocationReplaceInWindowOpenWithEmptyTarget): the web state’s last committed URL was "", the defocused location view was empty (LocationViewEmpty()), and the focused omnibox text was "". A page that the opener scripts could thus be shown with no URL in the omnibox, which is incorrect security UI. The broken invariant is that the URL Chromium records and displays must faithfully reflect the URL WebKit actually committed.
The fix reads [url dataRepresentation] whenever the feature is enabled. If those bytes differ from absoluteString and the scheme is about, it builds the GURL from the raw data, yielding about:blank#hash, so the omnibox now shows about:blank. For any other scheme a mismatch is only recorded in histograms and the old absoluteString path is still used.
NSURL to GURL conversion used absoluteString, which Foundation percent-encodes (# to %23) for about: URLs. This produced a URL Chromium did not treat as the committed about:blank document and left the omnibox blank. The fix uses the unmodified dataRepresentation for about: URLs when the two forms differ.Attack Path
- Open a popup
An attacker page calls
window.openwith an empty target and thenlocation.replaces the new window toabout:blank#hash, the scenario exercised bywebScenarioLocationReplaceInWindowOpenWithEmptyTargeton iOS. - URL mangled on conversion
WebKit reports the committed URL as an
NSURL.GURLWithNSURLreadsabsoluteStringand getsabout:blank%23hash, which per the old test results in an empty last-committed URL. - Blank omnibox
The location view and focused omnibox show no URL for the new tab, as the removed
LocationViewEmpty()/OmniboxText("")assertions confirm. - Spoofed content
The opener, which keeps script access to the same-origin
about:blankpopup, can write arbitrary content into it. The user sees no URL that identifies the content’s true provenance (inferred; the diff shows only the blank omnibox).
Impact Assessment
GURLWithNSURL) can open a tab whose omnibox shows an empty URL instead of about:blank. This weakens the user’s ability to judge which site controls the content and could aid phishing (inferred). It requires only that the user visit the attacker page and that popups are allowed. The advisory rates this Low (“Incorrect security UI in Omnibox”) and does not say it was exploited in the wild. No memory-safety issue is involved.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifnet/base/apple/url_conversions.mm |
modified | |
ifnet/base/apple/url_conversions_fuzzer.mm |
modified | |
TEST_Fnet/base/apple/url_conversions_unittest.mm |
modified | |
ifnet/BUILD.gn |
modified | |
fuzzer_testnet/BUILD.gn |
modified |
Files Changed
chrome/app_shim/app_shim_controller.mmios/chrome/browser/web/model/window_open_by_dom_egtest.mmnet/BUILD.gnnet/base/apple/url_conversions.mmnet/base/apple/url_conversions_fuzzer.mmnet/base/apple/url_conversions_unittest.mmnet/base/features.ccnet/base/features.htools/metrics/histograms/enums.xmltools/metrics/histograms/metadata/net/histograms.xml
Audit Directions
- Other `NSURL` accessorsThe patch comment says every
NSURLaccessor other thandataRepresentationmay re-encode. Audit iOS/macOS code that derivesGURLs, origins or display strings fromabsoluteString,host,pathorfragment(for exampleWKNavigationActionhandling andWKWebView.URLobservers) for similar mismatches. - Non-`about` scheme mismatchesThe fix only switches behavior for
Scheme::kAbout. TheNet.Apple.NSURL.DataMismatch.Schemehistogram exists to find others. Check whetherdata:,blob:,file:orjavascript:-adjacent URLs can likewise be re-encoded into something that displays misleadingly or bypasses scheme checks. - Empty or invalid committed URL fallbacksLook for omnibox and security-indicator code paths on iOS that render an empty or blank URL when the committed
GURLis invalid or unexpected, instead of failing closed or showing the opener’s origin.
Patch
diff --git a/chrome/app_shim/app_shim_controller.mm b/chrome/app_shim/app_shim_controller.mm
index de636b14..c3a78a7 100644
--- a/chrome/app_shim/app_shim_controller.mm
+++ b/chrome/app_shim/app_shim_controller.mm
@@ -293,6 +293,7 @@
"UseIDNAContextJRules", "MojoBindingsInlineSLS",
"MojoInlineMessagePayloads", "MojoIpcz", "MojoIpczMemV2",
"MojoFixGeometricBufferGrowth", "UseAdHocSigningForWebAppShims",
+ "UseNSURLDataForGURLConversion",
"SonomaAccessibilityActivationRefinements", "FeatureParamWithCache",
"UseMachVouchers"});
}
diff --git a/ios/chrome/browser/web/model/window_open_by_dom_egtest.mm b/ios/chrome/browser/web/model/window_open_by_dom_egtest.mm
index d59a009..fd89966 100644
--- a/ios/chrome/browser/web/model/window_open_by_dom_egtest.mm
+++ b/ios/chrome/browser/web/model/window_open_by_dom_egtest.mm
@@ -175,23 +175,15 @@
[ChromeEarlGrey tapWebStateElementWithID:
@"webScenarioLocationReplaceInWindowOpenWithEmptyTarget"];
[ChromeEarlGrey waitForMainTabCount:2];
- // WebKit doesn't parse 'about:blank#hash' as about:blank with URL fragment.
- // Instead, it percent encodes '#hash' and considers 'blank%23hash' as the
- // resource identifier. Nevertheless, the '#' is significant in triggering the
- // edge case in the bug. TODO(crbug.com/41414501): Change back to '#'.
- // Since about scheme URLs are also trimmed to about:blank, check the url
- // directly instead.
- //
- // TODO(crbug.com/40932726): Confirm the expected behavior of [ChromeEarlGrey
- // webStateLastCommittedURL] here. After https://crrev.com/c/4823237, this
- // returns empty URL ("").
- DCHECK_EQ("", [ChromeEarlGrey webStateLastCommittedURL]);
- // And confirm the location bar and focused omnibox are empty.
+ DCHECK_EQ(GURL("about:blank#hash"),
+ [ChromeEarlGrey webStateLastCommittedURL]);
+ // And confirm the location bar shows "about:blank".
[[EarlGrey selectElementWithMatcher:chrome_test_util::DefocusedLocationView()]
- assertWithMatcher:chrome_test_util::LocationViewEmpty()];
+ assertWithMatcher:chrome_test_util::LocationViewContainingText(
+ "about:blank")];
[ChromeEarlGreyUI focusOmnibox];
[[EarlGrey selectElementWithMatcher:chrome_test_util::Omnibox()]
- assertWithMatcher:chrome_test_util::OmniboxText("")];
+ assertWithMatcher:chrome_test_util::OmniboxText("about:blank")];
[OmniboxEarlGrey defocusOmnibox];
}
diff --git a/net/base/apple/url_conversions.mm b/net/base/apple/url_conversions.mm
index 8f38979..16481f1 100644
--- a/net/base/apple/url_conversions.mm
+++ b/net/base/apple/url_conversions.mm
@@ -6,10 +6,64 @@
#import <Foundation/Foundation.h>
+#include "base/containers/fixed_flat_map.h"
+#include "base/metrics/histogram_macros.h"
#include "base/strings/escape.h"
+#include "base/strings/sys_string_conversions.h"
+#include "net/base/features.h"
#include "url/gurl.h"
#include "url/url_canon.h"
+namespace {
+
+// Schemes that are tracked in the Net.Apple.NSURL.DataMismatch.Scheme
+// histogram. These values are persisted to logs. Entries should not be
+// renumbered and numeric values should never be reused.
+enum class Scheme {
+ kUnknown = 0,
+ kOther = 1,
+ kAbout = 2,
+ kBlob = 3,
+ kContent = 4,
+ kData = 5,
+ kFile = 6,
+ kFileSystem = 7,
+ kFtp = 8,
+ kHttp = 9,
+ kHttps = 10,
+ kMailto = 11,
+ kTel = 12,
+ kMaxValue = kTel,
+};
+
+Scheme SchemeForURL(NSURL* url) {
+ NSString* scheme = [url scheme];
+ if (!scheme) {
+ return Scheme::kUnknown;
+ }
+
+ static constexpr auto kSchemeMap =
+ base::MakeFixedFlatMap<std::string_view, Scheme>({
+ {"about", Scheme::kAbout},
+ {"blob", Scheme::kBlob},
+ {"content", Scheme::kContent},
+ {"data", Scheme::kData},
+ {"file", Scheme::kFile},
+ {"filesystem", Scheme::kFileSystem},
+ {"ftp", Scheme::kFtp},
+ {"http", Scheme::kHttp},
+ {"https", Scheme::kHttps},
+ {"mailto", Scheme::kMailto},
+ {"tel", Scheme::kTel},
+ });
+
+ std::string lower_scheme = base::SysNSStringToUTF8([scheme lowercaseString]);
+ auto it = kSchemeMap.find(lower_scheme);
+ return it != kSchemeMap.end() ? it->second : Scheme::kOther;
+}
+
+} // namespace
+
namespace net {
NSURL* NSURLWithGURL(const GURL& url) {
@@ -45,10 +99,41 @@
}
GURL GURLWithNSURL(NSURL* url) {
- if (url) {
- return GURL(url.absoluteString.UTF8String);
+ if (!url) {
+ return GURL();
}
- return GURL();
+
+ std::string_view standard_url_string(url.absoluteString.UTF8String);
+
+ // Foundation sometimes encodes the URL in absoluteString (and all the
+ // NSURL accessors other than dataRepresentation), which is not what we want
+ // to pass to GURL. For example, 'about:blank#hash' becomes
+ // 'about:blank%23hash' in absoluteString, but remains 'about:blank#hash' in
+ // dataRepresentation.
+ if (base::FeatureList::IsEnabled(features::kUseNSURLDataForGURLConversion)) {
+ NSData* data = [url dataRepresentation];
+ if (data && data.length > 0) {
+ std::string_view data_url_string(
+ reinterpret_cast<const char*>(data.bytes), data.length);
+ bool mismatch = standard_url_string != data_url_string;
+ // TODO(crbug.com/474953367): Consider removing these histograms after
+ // evaluation.
+ UMA_HISTOGRAM_BOOLEAN("Net.Apple.NSURL.DataMismatch", mismatch);
+ if (mismatch) {
+ Scheme scheme = SchemeForURL(url);
+ UMA_HISTOGRAM_ENUMERATION("Net.Apple.NSURL.DataMismatch.Scheme",
+ scheme);
+
+ // For "about:" scheme, use the data representation to avoid the
+ // encoding issue.
+ if (scheme == Scheme::kAbout) {
+ return GURL(data_url_string);
+ }
+ }
+ }
+ }
+
+ return GURL(standard_url_string);
}
} // namespace net
diff --git a/net/base/apple/url_conversions_fuzzer.mm b/net/base/apple/url_conversions_fuzzer.mm
new file mode 100644
index 0000000..b5ed948
--- /dev/null
+++ b/net/base/apple/url_conversions_fuzzer.mm
@@ -0,0 +1,34 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/base/apple/url_conversions.h"
+
+#import <Foundation/Foundation.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "base/check_op.h"
+#include "base/strings/sys_string_conversions.h"
+#include "url/gurl.h"
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) {
+ @autoreleasepool {
+ NSString* string = [[NSString alloc] initWithBytes:data
+ length:size
+ encoding:NSUTF8StringEncoding];
+ if (!string) {
+ return 0;
+ }
+
+ NSURL* url = [NSURL URLWithString:string];
+ if (url) {
+ GURL gurl_from_nsurl = net::GURLWithNSURL(url);
+ GURL gurl_from_string = GURL(base::SysNSStringToUTF8(string));
+ // A CHECK comparing gurl_from_nsurl gurl_from_string would not be
+ // useful because there are simply too many differences.
+ (void)(gurl_from_nsurl == gurl_from_string);
+ }
+ }
+ return 0;
+}
diff --git a/net/base/features.cc b/net/base/features.cc
index 40c72fd9..de9b6ab8 100644
--- a/net/base/features.cc
+++ b/net/base/features.cc
@@ -704,4 +704,8 @@
BASE_FEATURE(kUseLockFreeX509Verification, base::FEATURE_DISABLED_BY_DEFAULT);
+#if BUILDFLAG(IS_APPLE)
+BASE_FEATURE(kUseNSURLDataForGURLConversion, base::FEATURE_ENABLED_BY_DEFAULT);
+#endif // BUILDFLAG(IS_APPLE)
+
} // namespace net::features
diff --git a/net/base/features.h b/net/base/features.h
index 48f1da6e..7937e27 100644
--- a/net/base/features.h
+++ b/net/base/features.h
@@ -748,6 +748,12 @@
// lock-free certificate verification mechanism.
NET_EXPORT BASE_DECLARE_FEATURE(kUseLockFreeX509Verification);
+#if BUILDFLAG(IS_APPLE)
+// If enabled, the GURL conversion for NSURLs will use the data representation
+// of the URL if it differs from the absolute string.
+NET_EXPORT BASE_DECLARE_FEATURE(kUseNSURLDataForGURLConversion);
+#endif // BUILDFLAG(IS_APPLE)
+
} // namespace net::features
#endif // NET_BASE_FEATURES_H_
diff --git a/tools/metrics/histograms/enums.xml b/tools/metrics/histograms/enums.xml
index b8e69607..e77590b 100644
--- a/tools/metrics/histograms/enums.xml
+++ b/tools/metrics/histograms/enums.xml
@@ -21734,6 +21734,22 @@
<int value="8" label="H2 pings"/>
</enum>
+<enum name="NSURLScheme">
+ <int value="0" label="Unknown"/>
+ <int value="1" label="Other"/>
+ <int value="2" label="about"/>
+ <int value="3" label="blob"/>
+ <int value="4" label="content"/>
+ <int value="5" label="data"/>
+ <int value="6" label="file"/>
+ <int value="7" label="filesystem"/>
+ <int value="8" label="ftp"/>
+ <int value="9" label="http"/>
+ <int value="10" label="https"/>
+ <int value="11" label="mailto"/>
+ <int value="12" label="tel"/>
+</enum>
+
<enum name="NTPHistoryClustersModuleDisplayLayout">
<int value="0" label="Not displayed"/>
<int value="1" label="Layout 1"/>
diff --git a/tools/metrics/histograms/metadata/net/histograms.xml b/tools/metrics/histograms/metadata/net/histograms.xml
index 43a4bd6..884e70e 100644
--- a/tools/metrics/histograms/metadata/net/histograms.xml
+++ b/tools/metrics/histograms/metadata/net/histograms.xml
@@ -1494,6 +1494,29 @@
</summary>
</histogram>
+<histogram name="Net.Apple.NSURL.DataMismatch" enum="Boolean"
+ expires_after="2026-06-01">
+ <owner>[email protected]</owner>
+ <owner>[email protected]</owner>
+ <summary>
+ Records whether [NSURL dataRepresentation] and absoluteString give different
+ results during GURL conversion. Recorded each time the helper GURLWithNSURL
+ is called. This helps evaluate if switching to dataRepresentation is safe.
+ </summary>
+</histogram>
+
+<histogram name="Net.Apple.NSURL.DataMismatch.Scheme" enum="NSURLScheme"
+ expires_after="2026-06-01">
+ <owner>[email protected]</owner>
+ <owner>[email protected]</owner>
+ <summary>
+ Records the scheme of the URL when there is a mismatch between
+ absoluteString and dataRepresentation. Each time a ‘true’ value is recorded
+ for the Net.Apple.NSURL.DataMismatch histogram, one instance of this
+ histogram is recorded.
+ </summary>
+</histogram>
+
<histogram name="Net.BlobFetch.ResponseNetErrorCode" enum="NetErrorCodes"
expires_after="2026-06-28">
<owner>[email protected]</owner>
diff --git a/net/base/apple/url_conversions_unittest.mm b/net/base/apple/url_conversions_unittest.mm
index db0c83a3..180299fc 100644
--- a/net/base/apple/url_conversions_unittest.mm
+++ b/net/base/apple/url_conversions_unittest.mm
@@ -2,10 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#import "net/base/apple/url_conversions.h"
+
#import <Foundation/Foundation.h>
#include "base/strings/sys_string_conversions.h"
-#import "net/base/apple/url_conversions.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
+#include "net/base/features.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#include "url/gurl.h"
@@ -234,6 +238,33 @@
EXPECT_EQ(nil, NSURLWithGURL(url));
}
+TEST_F(URLConversionTest, TestGURLWithNSURLFeatureEnabled) {
+ base::HistogramTester histogram_tester;
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndEnableFeature(
+ features::kUseNSURLDataForGURLConversion);
+
+ NSURL* url = [NSURL URLWithString:@"about:blank#hash"];
+ GURL gurl = GURLWithNSURL(url);
+ EXPECT_EQ("about:blank#hash", gurl.spec());
+ histogram_tester.ExpectUniqueSample("Net.Apple.NSURL.DataMismatch", true, 1);
+}
+
+TEST_F(URLConversionTest, TestGURLWithNSURLFeatureDisabled) {
+ base::HistogramTester histogram_tester;
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndDisableFeature(
+ features::kUseNSURLDataForGURLConversion);
+
+ NSURL* url = [NSURL URLWithString:@"about:blank#hash"];
+ GURL gurl = GURLWithNSURL(url);
+ // If this test fails, it means Apple has fixed the bug in NSURL's
+ // absoluteString, and the kUseNSURLDataForGURLConversion workaround can be
+ // removed.
+ EXPECT_EQ("about:blank%23hash", gurl.spec());
+ histogram_tester.ExpectTotalCount("Net.Apple.NSURL.DataMismatch", 0);
+}
+
} // namespace
} // namespace net
diff --git a/net/BUILD.gn b/net/BUILD.gn
index ee245ae..669bb5f 100644
--- a/net/BUILD.gn
+++ b/net/BUILD.gn
@@ -4303,6 +4303,17 @@
]
}
+if (is_apple) {
+ fuzzer_test("net_url_conversions_fuzzer") {
+ sources = [ "base/apple/url_conversions_fuzzer.mm" ]
+ deps = [
+ "//base",
+ "//net",
+ ]
+ frameworks = [ "Foundation.framework" ]
+ }
+}
+
if (is_linux || is_chromeos) {
fuzzer_test("net_base_address_tracker_linux_fuzzer") {
sources = [ "base/address_tracker_linux_fuzzer.cc" ]
Regression Test / PoC
diff --git a/net/base/apple/url_conversions_unittest.mm b/net/base/apple/url_conversions_unittest.mm
index db0c83a3..180299fc 100644
--- a/net/base/apple/url_conversions_unittest.mm
+++ b/net/base/apple/url_conversions_unittest.mm
@@ -2,10 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#import "net/base/apple/url_conversions.h"
+
#import <Foundation/Foundation.h>
#include "base/strings/sys_string_conversions.h"
-#import "net/base/apple/url_conversions.h"
+#include "base/test/metrics/histogram_tester.h"
+#include "base/test/scoped_feature_list.h"
+#include "net/base/features.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#include "url/gurl.h"
@@ -234,6 +238,33 @@
EXPECT_EQ(nil, NSURLWithGURL(url));
}
+TEST_F(URLConversionTest, TestGURLWithNSURLFeatureEnabled) {
+ base::HistogramTester histogram_tester;
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndEnableFeature(
+ features::kUseNSURLDataForGURLConversion);
+
+ NSURL* url = [NSURL URLWithString:@"about:blank#hash"];
+ GURL gurl = GURLWithNSURL(url);
+ EXPECT_EQ("about:blank#hash", gurl.spec());
+ histogram_tester.ExpectUniqueSample("Net.Apple.NSURL.DataMismatch", true, 1);
+}
+
+TEST_F(URLConversionTest, TestGURLWithNSURLFeatureDisabled) {
+ base::HistogramTester histogram_tester;
+ base::test::ScopedFeatureList scoped_feature_list;
+ scoped_feature_list.InitAndDisableFeature(
+ features::kUseNSURLDataForGURLConversion);
+
+ NSURL* url = [NSURL URLWithString:@"about:blank#hash"];
+ GURL gurl = GURLWithNSURL(url);
+ // If this test fails, it means Apple has fixed the bug in NSURL's
+ // absoluteString, and the kUseNSURLDataForGURLConversion workaround can be
+ // removed.
+ EXPECT_EQ("about:blank%23hash", gurl.spec());
+ histogram_tester.ExpectTotalCount("Net.Apple.NSURL.DataMismatch", 0);
+}
+
} // namespace
} // namespace net
Original Bug Report
Blank Address Bar Spoofing on Chrome for iOS
Steps to reproduce the problem
- Open Chrome for iOS and visit this link: https://vxyoka.github.io/spoof/
- Click on Login Button
- You will notice blank address bar with fake contents.
Problem Description
Summary:
A UI spoofing issue exists in Chrome for iOS where navigating to a crafted about:blank URL results in a blank address bar
What is the expected behavior?
When navigating to about:blank, the address bar should explicitly display about:blank.
What went wrong?
Chrome for iOS displays a completely blank address bar
Summary
Blank Address Bar Spoofing on Chrome for iOS
Additional Data
Category: Security
Chrome Channel: Stable
Regression: N/A \