CVE-2026-106209
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fios/chrome/browser/lens_overlay/model/lens_overlay_navigation_manager_unittest.mm |
modified |
Files Changed
ios/chrome/browser/lens_overlay/model/lens_overlay_url_utils.mmios/chrome/browser/lens_overlay/model/lens_overlay_navigation_manager_unittest.mm
Patch
From 8b46a82f5193ad2c19fa98e988dae9f58d940b15 Mon Sep 17 00:00:00 2001
From: Stepan Khapugin <stkhapugin@google.com>
Date: Tue, 08 Sep 2026 13:19:29 -0700
Subject: [PATCH] [iOS][Lens] Check host in IsLensOverlaySRP.
Check in lens srp helpers that the host is google.
Fixed: 518039724
Change-Id: I4615345fb54eed3c412933a22018321006e7d647
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8372182
Reviewed-by: Charles Yang <cmyang@google.com>
Commit-Queue: Stepan Khapugin <stkhapugin@chromium.org>
Commit-Queue: Charles Yang <cmyang@google.com>
Auto-Submit: Stepan Khapugin <stkhapugin@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1694003}
---
diff --git a/ios/chrome/browser/lens_overlay/model/lens_overlay_url_utils.mm b/ios/chrome/browser/lens_overlay/model/lens_overlay_url_utils.mm
index 68e8df8..14def4a 100644
--- a/ios/chrome/browser/lens_overlay/model/lens_overlay_url_utils.mm
+++ b/ios/chrome/browser/lens_overlay/model/lens_overlay_url_utils.mm
@@ -27,6 +27,9 @@
}
bool IsLensOverlaySRP(const GURL& url) {
+ if (!IsGoogleHostURL(url)) {
+ return false;
+ }
std::string search_term;
bool hasSearchTerms = net::GetValueForKeyInQuery(url, "q", &search_term);
std::string lens_surface;
@@ -41,6 +44,9 @@
}
bool IsLensMultimodalSRP(const GURL& url) {
+ if (!IsGoogleHostURL(url)) {
+ return false;
+ }
std::string search_term;
bool has_search_terms = net::GetValueForKeyInQuery(url, "q", &search_term);
std::string lens_surface;
diff --git a/ios/chrome/browser/lens_overlay/model/lens_overlay_navigation_manager_unittest.mm b/ios/chrome/browser/lens_overlay/model/lens_overlay_navigation_manager_unittest.mm
index 322a9723..f37d8c8 100644
--- a/ios/chrome/browser/lens_overlay/model/lens_overlay_navigation_manager_unittest.mm
+++ b/ios/chrome/browser/lens_overlay/model/lens_overlay_navigation_manager_unittest.mm
@@ -16,6 +16,7 @@
#import "components/variations/variations_ids_provider.h"
#import "ios/chrome/browser/lens_overlay/coordinator/fake_chrome_lens_overlay_result.h"
#import "ios/chrome/browser/lens_overlay/model/lens_overlay_navigation_mutator.h"
+#import "ios/chrome/browser/lens_overlay/model/lens_overlay_url_utils.h"
#import "ios/chrome/common/NSString+Chromium.h"
#import "ios/web/public/test/fakes/fake_navigation_context.h"
#import "ios/web/public/test/fakes/fake_navigation_manager.h"
@@ -388,3 +389,48 @@
GoBackExpectingURLReload(URL2, /*expect_can_go_back=*/YES);
EXPECT_TRUE([latest_loaded_omnibox_text_ isEqualToString:text1]);
}
+
+// Tests that navigation to a non-Google domain does not update the omnibox
+// steady-state text.
+TEST_F(LensOverlayNavigationManagerTest,
+ NonGoogleHostURLDoesNotUpdateOmniboxText) {
+ id<ChromeLensOverlayResult> result1 = GenerateResult(/*identifier=*/1);
+ SimulateLensDidGenerateResult(result1, /*expect_load=*/YES,
+ /*expect_can_go_back=*/NO);
+
+ NSString* sample_query = @"sample search label";
+ GURL non_google_url = GURL("https://example.com/search");
+ non_google_url = net::AppendOrReplaceQueryParameter(
+ non_google_url, "q", sample_query.cr_UTF8String);
+ non_google_url = net::AppendOrReplaceQueryParameter(
+ non_google_url, lens::kLensSurfaceQueryParameter, "4");
+
+ // Mutator should not receive onSRPLoadWithOmniboxText: for non-Google hosts.
+ [[mock_mutator_ reject] onSRPLoadWithOmniboxText:[OCMArg any]
+ isMultimodal:NO];
+ [[mock_mutator_ reject] onSRPLoadWithOmniboxText:[OCMArg any]
+ isMultimodal:YES];
+ SimulateWebNavigation(non_google_url, /*expect_can_go_back=*/YES);
+ EXPECT_OCMOCK_VERIFY(mock_mutator_);
+
+ // Direct verification of URL utils.
+ EXPECT_FALSE(lens::IsLensOverlaySRP(non_google_url));
+ EXPECT_FALSE(lens::IsLensMultimodalSRP(non_google_url));
+}
+
+// Tests that IsLensOverlaySRP and IsLensMultimodalSRP require a Google host.
+TEST_F(LensOverlayNavigationManagerTest, GoogleHostURLValidation) {
+ EXPECT_TRUE(lens::IsLensOverlaySRP(
+ GURL("https://www.google.com/search?q=test&lns_surface=4")));
+ EXPECT_TRUE(lens::IsLensOverlaySRP(
+ GURL("https://www.google.co.uk/search?q=test&lns_surface=4")));
+ EXPECT_FALSE(lens::IsLensOverlaySRP(
+ GURL("https://example.com/search?q=test&lns_surface=4")));
+ EXPECT_FALSE(lens::IsLensOverlaySRP(
+ GURL("https://google.com.example.com/search?q=test&lns_surface=4")));
+
+ EXPECT_TRUE(lens::IsLensMultimodalSRP(GURL(
+ "https://www.google.com/search?q=test&lns_surface=4&vsrid=123&udm=24")));
+ EXPECT_FALSE(lens::IsLensMultimodalSRP(GURL(
+ "https://example.com/search?q=test&lns_surface=4&vsrid=123&udm=24")));
+}
Regression Test / PoC
diff --git a/ios/chrome/browser/lens_overlay/model/lens_overlay_navigation_manager_unittest.mm b/ios/chrome/browser/lens_overlay/model/lens_overlay_navigation_manager_unittest.mm
index 322a9723..f37d8c8 100644
--- a/ios/chrome/browser/lens_overlay/model/lens_overlay_navigation_manager_unittest.mm
+++ b/ios/chrome/browser/lens_overlay/model/lens_overlay_navigation_manager_unittest.mm
@@ -16,6 +16,7 @@
#import "components/variations/variations_ids_provider.h"
#import "ios/chrome/browser/lens_overlay/coordinator/fake_chrome_lens_overlay_result.h"
#import "ios/chrome/browser/lens_overlay/model/lens_overlay_navigation_mutator.h"
+#import "ios/chrome/browser/lens_overlay/model/lens_overlay_url_utils.h"
#import "ios/chrome/common/NSString+Chromium.h"
#import "ios/web/public/test/fakes/fake_navigation_context.h"
#import "ios/web/public/test/fakes/fake_navigation_manager.h"
@@ -388,3 +389,48 @@
GoBackExpectingURLReload(URL2, /*expect_can_go_back=*/YES);
EXPECT_TRUE([latest_loaded_omnibox_text_ isEqualToString:text1]);
}
+
+// Tests that navigation to a non-Google domain does not update the omnibox
+// steady-state text.
+TEST_F(LensOverlayNavigationManagerTest,
+ NonGoogleHostURLDoesNotUpdateOmniboxText) {
+ id<ChromeLensOverlayResult> result1 = GenerateResult(/*identifier=*/1);
+ SimulateLensDidGenerateResult(result1, /*expect_load=*/YES,
+ /*expect_can_go_back=*/NO);
+
+ NSString* sample_query = @"sample search label";
+ GURL non_google_url = GURL("https://example.com/search");
+ non_google_url = net::AppendOrReplaceQueryParameter(
+ non_google_url, "q", sample_query.cr_UTF8String);
+ non_google_url = net::AppendOrReplaceQueryParameter(
+ non_google_url, lens::kLensSurfaceQueryParameter, "4");
+
+ // Mutator should not receive onSRPLoadWithOmniboxText: for non-Google hosts.
+ [[mock_mutator_ reject] onSRPLoadWithOmniboxText:[OCMArg any]
+ isMultimodal:NO];
+ [[mock_mutator_ reject] onSRPLoadWithOmniboxText:[OCMArg any]
+ isMultimodal:YES];
+ SimulateWebNavigation(non_google_url, /*expect_can_go_back=*/YES);
+ EXPECT_OCMOCK_VERIFY(mock_mutator_);
+
+ // Direct verification of URL utils.
+ EXPECT_FALSE(lens::IsLensOverlaySRP(non_google_url));
+ EXPECT_FALSE(lens::IsLensMultimodalSRP(non_google_url));
+}
+
+// Tests that IsLensOverlaySRP and IsLensMultimodalSRP require a Google host.
+TEST_F(LensOverlayNavigationManagerTest, GoogleHostURLValidation) {
+ EXPECT_TRUE(lens::IsLensOverlaySRP(
+ GURL("https://www.google.com/search?q=test&lns_surface=4")));
+ EXPECT_TRUE(lens::IsLensOverlaySRP(
+ GURL("https://www.google.co.uk/search?q=test&lns_surface=4")));
+ EXPECT_FALSE(lens::IsLensOverlaySRP(
+ GURL("https://example.com/search?q=test&lns_surface=4")));
+ EXPECT_FALSE(lens::IsLensOverlaySRP(
+ GURL("https://google.com.example.com/search?q=test&lns_surface=4")));
+
+ EXPECT_TRUE(lens::IsLensMultimodalSRP(GURL(
+ "https://www.google.com/search?q=test&lns_surface=4&vsrid=123&udm=24")));
+ EXPECT_FALSE(lens::IsLensMultimodalSRP(GURL(
+ "https://example.com/search?q=test&lns_surface=4&vsrid=123&udm=24")));
+}
Original Bug Report
Omnibox text spoofing in iOS Chrome Lens results sheet via Google AMP viewer links
Project Fortify, 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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential address bar and origin-indicator spoofing vulnerability exists in iOS Chrome’s Lens results bottom sheet UI. Due to missing host and path validation in IsLensOverlaySRP, an attacker can leverage a Google AMP viewer link with the lns_surface=4 parameter to bypass domain restrictions while loading their own phishing page. This allows the attacker to fully control the native omnibox steady-state text using the q query parameter of the navigated URL.
Affected files:
ios/chrome/browser/lens_overlay/model/lens_overlay_url_utils.mmios/chrome/browser/lens_overlay/model/lens_overlay_navigation_manager.mmios/chrome/browser/lens_overlay/coordinator/lens_result_page_mediator.mmios/chrome/browser/lens_overlay/coordinator/lens_overlay_mediator.mmios/chrome/browser/lens_overlay/coordinator/lens_omnibox_client.mm
Estimated timestamp from git blame: 2025-01-10
Description
A potential address-bar/origin-indicator spoofing vulnerability exists in the iOS Chrome Lens results panel. When a user navigates inside the results panel, the policy decider IsValidURLToOpenInResultsPage permits navigation to any URL on an allowed Google host that contains the lns_surface=4 parameter. An attacker can exploit this by utilizing a Google AMP viewer link (e.g., https://www.google.com/amp/s/attacker.com/phish) to bypass the host restriction and load an attacker-controlled page.
Once the navigation begins, LensOverlayNavigationManager::DidStartNavigation evaluates the destination URL using lens::IsLensOverlaySRP. However, IsLensOverlaySRP only checks query parameters (such as q and lns_surface=4) and performs no host or path validation. As a result, the navigation manager incorrectly identifies the AMP URL as a Search Results Page (SRP) and extracts the q query parameter to override the native omnibox steady-state text via LensOmniboxClient::SetOmniboxSteadyStateText. The native omnibox then displays the attacker-controlled text (e.g., "chase.com — Sign in") while the web view loads the attacker’s phishing content inside the AMP viewer.
Potential Steps to Reproduce
- On iOS Chrome, invoke the Lens overlay and wait for the results bottom sheet to load.
- Trigger a main-frame navigation within the panel’s WebState to a crafted Google AMP URL, such as:
https://www.google.com/amp/s/attacker.com/phish?q=chase.com%20%E2%80%94%20Sign%20in&lns_surface=4 - Observe that the navigation is permitted because
www.google.comis a valid Google domain underIsGoogleHostURL(which permits thewww.googleprefix underDISALLOW_SUBDOMAINchecks) and containslns_surface=4. - Observe that
IsLensOverlaySRPevaluates the URL as a valid SRP because it contains query keysqandlns_surface=4. - Observe that the native omnibox text is rewritten to display
chase.com — Sign inwhile the page viewport displays the attacker’s phishing page.
Note: These are potential steps analyzed from the source code, as our tooling environment does not currently have the capability to execute code or run dynamic proofs of concept.
Suggested Fix
Update IsLensOverlaySRP and IsLensMultimodalSRP in ios/chrome/browser/lens_overlay/model/lens_overlay_url_utils.mm to ensure they perform host validation using IsGoogleHostURL(url) before parsing and trusting query parameters:
bool IsLensOverlaySRP(const GURL& url) {
if (!IsGoogleHostURL(url)) {
return false;
}
std::string search_term;
bool hasSearchTerms = net::GetValueForKeyInQuery(url, "q", &search_term);
std::string lens_surface;
bool hasLensSurface = net::GetValueForKeyInQuery(
url, lens::kLensSurfaceQueryParameter, &lens_surface);
std::string request_id;
bool hasLensParam = net::GetValueForKeyInQuery(
url, lens::kLensRequestQueryParameter, &request_id);
return hasSearchTerms && hasLensSurface && !hasLensParam &&
lens_surface == "4";
}
Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.