CVE-2026-11205
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifios/chrome/browser/qr_scanner/ui/qr_scanner_view_controller.mm |
modified |
Files Changed
ios/chrome/browser/qr_scanner/test/qr_scanner_view_controller_egtest.mmios/chrome/browser/qr_scanner/ui/qr_scanner_view_controller.mm
Patch
From 3098962882ad76e8fcd3f4d5cc714c0918aad7cc Mon Sep 17 00:00:00 2001
From: Olivier Robin <olivierrobin@google.com>
Date: Tue, 28 Apr 2026 07:32:06 -0700
Subject: [PATCH] Remove spaces from QR code scan
Sanitize spaces from QR code scaner results
- Replace all whitespace characters with spaces
- Remove duplicate spaces
- Remove leading and trailing spaces
Bug: 505290253
Change-Id: Ib8b121734475e8e8c61a627d34b631e4ab6bbc71
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7796322
Auto-Submit: Olivier Robin <olivierrobin@chromium.org>
Reviewed-by: Quentin Pubert <qpubert@google.com>
Commit-Queue: Olivier Robin <olivierrobin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1621749}
---
diff --git a/ios/chrome/browser/qr_scanner/test/qr_scanner_view_controller_egtest.mm b/ios/chrome/browser/qr_scanner/test/qr_scanner_view_controller_egtest.mm
index 8999bd8..8b0db1e 100644
--- a/ios/chrome/browser/qr_scanner/test/qr_scanner_view_controller_egtest.mm
+++ b/ios/chrome/browser/qr_scanner/test/qr_scanner_view_controller_egtest.mm
@@ -705,4 +705,23 @@
sanitizedResult:kTestSanitizedDataURL];
}
+// Test that whitespace is trimmed and duplicate spaces are removed.
+- (void)testQueryWhitespaceSanitized {
+ [self doTestReceivingResult:" \t a \n b " sanitizedResult:"a b"];
+}
+
+// Test that whitespace is trimmed from URLs.
+- (void)testURLWhitespaceTrimmed {
+ std::string input = " \t\u3000 " + _testURL.spec() + " \n ";
+ [self doTestReceivingResult:input
+ sanitizedResult:"\"" + _testURL.spec() + "\""];
+}
+
+// Test that whitespace is trimmed from not http/https URLs and quotes are
+// added.
+- (void)testDataURLWhitespaceTrimmed {
+ std::string input = " \t\u3000 " + std::string(kTestDataURL) + " \n ";
+ [self doTestReceivingResult:input sanitizedResult:kTestSanitizedDataURL];
+}
+
@end
diff --git a/ios/chrome/browser/qr_scanner/ui/qr_scanner_view_controller.mm b/ios/chrome/browser/qr_scanner/ui/qr_scanner_view_controller.mm
index 4d3cbbc..d5da302b 100644
--- a/ios/chrome/browser/qr_scanner/ui/qr_scanner_view_controller.mm
+++ b/ios/chrome/browser/qr_scanner/ui/qr_scanner_view_controller.mm
@@ -68,12 +68,12 @@
#pragma mark - QRScannerCameraControllerDelegate
-- (void)receiveQRScannerResult:(NSString*)result loadImmediately:(BOOL)load {
- result = [self sanitizedStringWithString:result];
-
+- (void)receiveQRScannerResult:(NSString*)rawResult loadImmediately:(BOOL)load {
+ NSString* result = [self sanitizedStringWithString:rawResult];
+ BOOL sanitized = ![result isEqualToString:rawResult];
GURL url = GURL(base::SysNSStringToUTF8(result));
- if (url.is_valid() && !url.SchemeIsHTTPOrHTTPS()) {
- // Only HTTP(S) URLs are supported.
+ if (url.is_valid() && (!url.SchemeIsHTTPOrHTTPS() || sanitized)) {
+ // Only unmodified HTTP(S) URLs are supported.
// For other URLs, add quotes so they are considered as search terms instead
// of URLs.
result = [NSString stringWithFormat:@"\"%@\"", result];
@@ -133,6 +133,11 @@
// Remove characters that might confuse users when originating from a QR code.
- (NSString*)sanitizedStringWithString:(NSString*)string {
+ if (!string) {
+ return @"";
+ }
+
+ // Remove control, newline, non-base, and illegal characters.
NSMutableCharacterSet* badCharacters =
[NSMutableCharacterSet controlCharacterSet];
[badCharacters
@@ -141,8 +146,19 @@
formUnionWithCharacterSet:[NSCharacterSet nonBaseCharacterSet]];
[badCharacters
formUnionWithCharacterSet:[NSCharacterSet illegalCharacterSet]];
- return [[string componentsSeparatedByCharactersInSet:badCharacters]
- componentsJoinedByString:@""];
+
+ NSString* cleaned =
+ [[string componentsSeparatedByCharactersInSet:badCharacters]
+ componentsJoinedByString:@""];
+
+ // Replace whitespace with spaces, trim, and remove duplicates.
+ NSArray* components =
+ [cleaned componentsSeparatedByCharactersInSet:
+ [NSCharacterSet whitespaceAndNewlineCharacterSet]];
+ NSPredicate* predicate = [NSPredicate predicateWithFormat:@"SELF != ''"];
+ NSArray* filteredComponents =
+ [components filteredArrayUsingPredicate:predicate];
+ return [filteredComponents componentsJoinedByString:@" "];
}
#pragma mark - Testing Additions
Regression Test / PoC
diff --git a/ios/chrome/browser/qr_scanner/test/qr_scanner_view_controller_egtest.mm b/ios/chrome/browser/qr_scanner/test/qr_scanner_view_controller_egtest.mm
index 8999bd8..8b0db1e 100644
--- a/ios/chrome/browser/qr_scanner/test/qr_scanner_view_controller_egtest.mm
+++ b/ios/chrome/browser/qr_scanner/test/qr_scanner_view_controller_egtest.mm
@@ -705,4 +705,23 @@
sanitizedResult:kTestSanitizedDataURL];
}
+// Test that whitespace is trimmed and duplicate spaces are removed.
+- (void)testQueryWhitespaceSanitized {
+ [self doTestReceivingResult:" \t a \n b " sanitizedResult:"a b"];
+}
+
+// Test that whitespace is trimmed from URLs.
+- (void)testURLWhitespaceTrimmed {
+ std::string input = " \t\u3000 " + _testURL.spec() + " \n ";
+ [self doTestReceivingResult:input
+ sanitizedResult:"\"" + _testURL.spec() + "\""];
+}
+
+// Test that whitespace is trimmed from not http/https URLs and quotes are
+// added.
+- (void)testDataURLWhitespaceTrimmed {
+ std::string input = " \t\u3000 " + std::string(kTestDataURL) + " \n ";
+ [self doTestReceivingResult:input sanitizedResult:kTestSanitizedDataURL];
+}
+
@end
Original Bug Report
Universal XSS via iOS QR Scanner Parser Differential
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 go/chrome-ai-generated-security-bugs-faq for more information.
Overview: The iOS QR scanner’s validation to prevent javascript: and restricted scheme execution can be bypassed using an Ideographic Space character (U+3000). A parser differential between GURL and AutocompleteInput allows an attacker-controlled QR code to execute arbitrary JavaScript in the context of the currently active website.
Affected files:
ios/chrome/browser/qr_scanner/ui/qr_scanner_view_controller.mm
Estimated timestamp from git blame: 2021-03-03
The iOS QR Scanner (ios/chrome/browser/qr_scanner/ui/qr_scanner_view_controller.mm) contains a security mechanism to prevent a scanned string from automatically executing javascript: or navigating to sensitive chrome:// URLs. The implementation parses the string using GURL and, if it is valid but not HTTP/HTTPS, mitigates the threat by wrapping the string in double quotes before passing it to the Omnibox. This forces the Omnibox to treat the payload as a web search rather than a URL.
However, this defense relies on the assumption that GURL and the Omnibox’s URL parser (AutocompleteInput) handle leading whitespace consistently. They do not:
GURLusesShouldTrimFromUrl(url/url_parse_internal.h), which only strips ASCII whitespace characters (<= 0x20). It fails to strip the Unicode Ideographic Space (U+3000). Consequently, a payload like" javascript:alert(1)"fails scheme extraction inGURLbecause the non-ASCII character makes the scheme invalid.url.is_valid()returnsfalse, so the mitigation is skipped and the string remains unquoted.- The unquoted string is then programmatically inserted into the Omnibox. Because the insertion is programmatic, it bypasses the
omnibox::SanitizeTextForPastecheck (which normally stripsjavascript:schemas on paste). - When the user taps “Go”,
AutocompleteInput::Init(components/omnibox/browser/autocomplete_input.cc) processes the string. It callsbase::TrimWhitespacewithbase::kWhitespaceUTF16, which explicitly includesU+3000. The Ideographic Space is cleanly stripped, leaving a validjavascript:URL. - The Omnibox executes the JavaScript payload in the context of the currently active WebState, resulting in a Universal Cross-Site Scripting (UXSS) vulnerability.
Potential Attack Scenario
- An attacker crafts a malicious QR code containing
" javascript:alert(document.cookie)". - The victim navigates to a sensitive site (e.g., their bank) in Chrome for iOS.
- The victim opens the Chrome QR scanner and scans the malicious code.
- The scanner fills the Omnibox with the unquoted string. The victim taps the “Go” button on the keyboard.
- The payload executes in the context of the bank’s origin.
Suggested Fix
Ensure that the QR scanner’s sanitization process aggressively trims all Unicode whitespace before validating the URL with GURL. Additionally, receiveQRScannerResult:loadImmediately: should wrap the result in quotes if the URL is invalid but appears to contain a restricted scheme, or if it is unable to be cleanly validated.
Evaluated with Chrome root at commit: 4a3e9db74111a3c6c4b3acfd70050a05077cf27a
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.