Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Chrome for iOS
DescriptionInsufficient validation of untrusted input in Chrome for iOS
ComponentChrome for iOS
Bug ClassLogic Error
Tracker505290253
Fix commit3098962882ad (chromium/src) +42/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
ios/chrome/browser/qr_scanner/ui/qr_scanner_view_controller.mm
modified

Files Changed

  • ios/chrome/browser/qr_scanner/test/qr_scanner_view_controller_egtest.mm
  • ios/chrome/browser/qr_scanner/ui/qr_scanner_view_controller.mm
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by li...@chromium.org

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:

  1. GURL uses ShouldTrimFromUrl (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 in GURL because the non-ASCII character makes the scheme invalid. url.is_valid() returns false, so the mitigation is skipped and the string remains unquoted.
  2. The unquoted string is then programmatically inserted into the Omnibox. Because the insertion is programmatic, it bypasses the omnibox::SanitizeTextForPaste check (which normally strips javascript: schemas on paste).
  3. When the user taps “Go”, AutocompleteInput::Init (components/omnibox/browser/autocomplete_input.cc) processes the string. It calls base::TrimWhitespace with base::kWhitespaceUTF16, which explicitly includes U+3000. The Ideographic Space is cleanly stripped, leaving a valid javascript: URL.
  4. 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

  1. An attacker crafts a malicious QR code containing " javascript:alert(document.cookie)".
  2. The victim navigates to a sensitive site (e.g., their bank) in Chrome for iOS.
  3. The victim opens the Chrome QR scanner and scans the malicious code.
  4. The scanner fills the Omnibox with the unquoted string. The victim taps the “Go” button on the keyboard.
  5. 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.

View on issue tracker