CVE-2026-106396
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
switchchrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/action/OmniboxActionInSuggest.java |
modified | |
ifchrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/action/OmniboxActionInSuggest.java |
modified | |
forchrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/action/OmniboxActionInSuggestUnitTest.java |
modified | |
switchcomponents/omnibox/browser/actions/omnibox_action_in_suggest.cc |
modified | |
TEST_Fcomponents/omnibox/browser/actions/omnibox_action_in_suggest_unittest.cc |
modified | |
forcomponents/omnibox/browser/actions/omnibox_action_in_suggest_unittest.cc |
modified |
Files Changed
components/omnibox/browser/actions/omnibox_action_in_suggest_unittest.cccomponents/omnibox/browser/actions/omnibox_action_in_suggest.ccchrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/action/OmniboxActionInSuggestUnitTest.javachrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/action/OmniboxActionInSuggest.java
Patch
From e5cd3f1d9d3042ea05e5d851035214fca4eee941 Mon Sep 17 00:00:00 2001
From: Tomasz Wiszkowski <ender@google.com>
Date: Tue, 08 Sep 2026 17:47:57 -0700
Subject: [PATCH] [Omnibox] Validate action URI schemes in OmniboxActionInSuggest
Malicious or untrusted suggest responses could supply an OmniboxAction
with a javascript: URI. For actions like CHROME_AIM, REVIEWS, or fallback
cases in DIRECTIONS and CHROME_TAB_SWITCH, Android executed these URIs
via delegate.loadPageInCurrentTab(), allowing arbitrary JavaScript
execution in the active tab without an initiator origin (UXSS).
Fix this by:
1. Enforcing strict URI scheme validation in C++ OmniboxActionInSuggest:
unconditionally reject javascript: schemes, require HTTP/HTTPS for
web actions (DIRECTIONS, REVIEWS, CHROME_AIM, CHROME_LENS,
CHROME_TAB_SWITCH), require tel: for CALL, and fail closed (return
false) for unhandled action types.
2. Adding defense-in-depth scheme validation in OmniboxActionInSuggest.java
before delegating page loads, ensuring javascript: schemes cannot be
dispatched to current tabs.
Change-Id: Ied9a190404468ebb7d6648df96c47607e0e20541
Fixed: 552423127
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8373185
Reviewed-by: Charles Cai <charlesyc@google.com>
Commit-Queue: Tomasz Wiszkowski <ender@google.com>
Reviewed-by: Sky Malice <skym@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1694240}
---
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/action/OmniboxActionInSuggest.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/action/OmniboxActionInSuggest.java
index fb93e95a53..fa989145 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/action/OmniboxActionInSuggest.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/action/OmniboxActionInSuggest.java
@@ -4,14 +4,14 @@
package org.chromium.chrome.browser.omnibox.suggestions.action;
-import static org.chromium.build.NullUtil.assumeNonNull;
-
import android.content.Intent;
import android.util.SparseArray;
import org.chromium.build.annotations.NullMarked;
+import org.chromium.build.annotations.Nullable;
import org.chromium.chrome.browser.omnibox.OmniboxMetrics;
import org.chromium.chrome.browser.omnibox.R;
+import org.chromium.components.embedder_support.util.UrlConstants;
import org.chromium.components.omnibox.SuggestTemplateInfoProto.SuggestTemplateInfo;
import org.chromium.components.omnibox.action.ActionPresentationMode;
import org.chromium.components.omnibox.action.OmniboxAction;
@@ -113,7 +113,7 @@
switch (actionType) {
case SuggestTemplateInfo.TemplateAction.ActionType.REVIEWS_VALUE:
case SuggestTemplateInfo.TemplateAction.ActionType.CHROME_AIM_VALUE:
- delegate.loadPageInCurrentTab(assumeNonNull(intent.getDataString()));
+ loadPageInCurrentTab(delegate, intent.getDataString());
actionStarted = true;
break;
@@ -137,7 +137,7 @@
case SuggestTemplateInfo.TemplateAction.ActionType.CHROME_TAB_SWITCH_VALUE:
if (!delegate.switchToTab(tabId, new GURL(mActionUri))) {
- delegate.loadPageInCurrentTab(assumeNonNull(intent.getDataString()));
+ loadPageInCurrentTab(delegate, intent.getDataString());
}
actionStarted = true;
break;
@@ -159,10 +159,22 @@
}
if (actionType == SuggestTemplateInfo.TemplateAction.ActionType.DIRECTIONS_VALUE) {
- delegate.loadPageInCurrentTab(assumeNonNull(intent.getDataString()));
+ loadPageInCurrentTab(delegate, intent.getDataString());
}
}
return true;
}
+
+ /**
+ * Loads the target URL in the current tab via the delegate if the URL is valid and safe.
+ * Rejects javascript: URLs to prevent cross-site scripting vulnerabilities.
+ */
+ private static void loadPageInCurrentTab(OmniboxActionDelegate delegate, @Nullable String url) {
+ if (url == null) return;
+ var gurl = new GURL(url);
+ if (gurl.isValid() && !UrlConstants.JAVASCRIPT_SCHEME.equalsIgnoreCase(gurl.getScheme())) {
+ delegate.loadPageInCurrentTab(url);
+ }
+ }
}
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/action/OmniboxActionInSuggestUnitTest.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/action/OmniboxActionInSuggestUnitTest.java
index a7c8a674..340f77e3 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/action/OmniboxActionInSuggestUnitTest.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/action/OmniboxActionInSuggestUnitTest.java
@@ -9,7 +9,9 @@
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThrows;
import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -343,6 +345,36 @@
}
@Test
+ public void executeActionInSuggest_rejectsJavascriptUrlForAim() {
+ var intent = new Intent(Intent.ACTION_VIEW).setData(Uri.parse("javascript:alert(1)"));
+ buildActionInSuggest(ActionType.CHROME_AIM, intent).execute(mDelegate);
+ verify(mDelegate, never()).loadPageInCurrentTab(any());
+ }
+
+ @Test
+ public void executeActionInSuggest_rejectsJavascriptUrlForReviews() {
+ var intent = new Intent(Intent.ACTION_VIEW).setData(Uri.parse("javascript:alert(1)"));
+ buildActionInSuggest(ActionType.REVIEWS, intent).execute(mDelegate);
+ verify(mDelegate, never()).loadPageInCurrentTab(any());
+ }
+
+ @Test
+ public void executeActionInSuggest_rejectsJavascriptUrlForTabSwitchFallback() {
+ doReturn(false).when(mDelegate).switchToTab(anyInt(), any());
+ var intent = new Intent(Intent.ACTION_VIEW).setData(Uri.parse("javascript:alert(1)"));
+ buildActionInSuggest(ActionType.CHROME_TAB_SWITCH, intent).execute(mDelegate);
+ verify(mDelegate, never()).loadPageInCurrentTab(any());
+ }
+
+ @Test
+ public void executeActionInSuggest_rejectsJavascriptUrlForDirectionsFallback() {
+ doReturn(true).when(mDelegate).isIncognito();
+ var intent = new Intent(Intent.ACTION_VIEW).setData(Uri.parse("javascript:alert(1)"));
+ buildActionInSuggest(ActionType.DIRECTIONS, intent).execute(mDelegate);
+ verify(mDelegate, never()).loadPageInCurrentTab(any());
+ }
+
+ @Test
public void getDisposition() {
for (var actionType : sKnownActionTypes) {
var action =
diff --git a/components/omnibox/browser/actions/omnibox_action_in_suggest.cc b/components/omnibox/browser/actions/omnibox_action_in_suggest.cc
index c90aae3..3ee24527 100644
--- a/components/omnibox/browser/actions/omnibox_action_in_suggest.cc
+++ b/components/omnibox/browser/actions/omnibox_action_in_suggest.cc
@@ -143,14 +143,24 @@
return false;
}
+ // Block javascript: schemes unconditionally to prevent XSS in the active tab
+ // context.
+ if (action_url.SchemeIs(url::kJavaScriptScheme)) {
+ return false;
+ }
+
switch (action_type) {
case omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CALL:
return action_url.SchemeIs(url::kTelScheme);
case omnibox::SuggestTemplateInfo_TemplateAction_ActionType_DIRECTIONS:
case omnibox::SuggestTemplateInfo_TemplateAction_ActionType_REVIEWS:
+ case omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_AIM:
+ case omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_LENS:
+ case omnibox::
+ SuggestTemplateInfo_TemplateAction_ActionType_CHROME_TAB_SWITCH:
return action_url.SchemeIsHTTPOrHTTPS();
default:
- return true;
+ return false;
}
}
} // namespace
diff --git a/components/omnibox/browser/actions/omnibox_action_in_suggest_unittest.cc b/components/omnibox/browser/actions/omnibox_action_in_suggest_unittest.cc
index 03f8c53..4ce741f 100644
--- a/components/omnibox/browser/actions/omnibox_action_in_suggest_unittest.cc
+++ b/components/omnibox/browser/actions/omnibox_action_in_suggest_unittest.cc
@@ -46,6 +46,8 @@
return "Reviews";
case omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_AIM:
return "Aim";
+ case omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_LENS:
+ return "Lens";
case omnibox::
SuggestTemplateInfo_TemplateAction_ActionType_CHROME_TAB_SWITCH:
return "TabSwitch";
@@ -308,6 +310,66 @@
}
}
+TEST_F(OmniboxActionInSuggestTest, Create_ValidatesUriSchemes) {
+ // CALL actions require tel: scheme.
+ {
+ TemplateAction action;
+ action.set_action_type(
+ omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CALL);
+ action.set_action_uri("tel:1234567890");
+ EXPECT_NE(nullptr, OmniboxActionInSuggest::Create(action, std::nullopt));
+
+ action.set_action_uri("https://google.com");
+ EXPECT_EQ(nullptr, OmniboxActionInSuggest::Create(action, std::nullopt));
+
+ action.set_action_uri("javascript:alert(1)");
+ EXPECT_EQ(nullptr, OmniboxActionInSuggest::Create(action, std::nullopt));
+ }
+
+ // Web actions require HTTP or HTTPS scheme.
+ const ActionType web_action_types[] = {
+ omnibox::SuggestTemplateInfo_TemplateAction_ActionType_DIRECTIONS,
+ omnibox::SuggestTemplateInfo_TemplateAction_ActionType_REVIEWS,
+ omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_AIM,
+ omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_LENS,
+ omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_TAB_SWITCH,
+ };
+
+ for (auto type : web_action_types) {
+ TemplateAction action;
+ action.set_action_type(type);
+
+ action.set_action_uri("https://www.google.com");
+ EXPECT_NE(nullptr, OmniboxActionInSuggest::Create(action, std::nullopt))
+ << "Failed for type: " << ToString(type);
+
+ action.set_action_uri("http://www.google.com");
+ EXPECT_NE(nullptr, OmniboxActionInSuggest::Create(action, std::nullopt))
+ << "Failed for type: " << ToString(type);
+
+ action.set_action_uri("javascript:alert(1)");
+ EXPECT_EQ(nullptr, OmniboxActionInSuggest::Create(action, std::nullopt))
+ << "Failed for type: " << ToString(type);
+
+ action.set_action_uri("data:text/html,<h1>XSS</h1>");
+ EXPECT_EQ(nullptr, OmniboxActionInSuggest::Create(action, std::nullopt))
+ << "Failed for type: " << ToString(type);
+
+ action.set_action_uri("tel:1234567890");
+ EXPECT_EQ(nullptr, OmniboxActionInSuggest::Create(action, std::nullopt))
+ << "Failed for type: " << ToString(type);
+ }
+
+ // Empty URI is allowed (e.g. for actions with search parameters).
+ {
+ TemplateAction action;
+ action.set_action_type(
+ omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_AIM);
+ action.set_action_uri("");
+ EXPECT_NE(nullptr, OmniboxActionInSuggest::Create(action, std::nullopt));
+ }
+}
+
#if BUILDFLAG(IS_ANDROID)
TEST_F(OmniboxActionInSuggestTest, ShowAsActionButtonForTabSwitch) {
TemplateAction template_action;
Regression Test / PoC
diff --git a/components/omnibox/browser/actions/omnibox_action_in_suggest_unittest.cc b/components/omnibox/browser/actions/omnibox_action_in_suggest_unittest.cc
index 03f8c53..4ce741f 100644
--- a/components/omnibox/browser/actions/omnibox_action_in_suggest_unittest.cc
+++ b/components/omnibox/browser/actions/omnibox_action_in_suggest_unittest.cc
@@ -46,6 +46,8 @@
return "Reviews";
case omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_AIM:
return "Aim";
+ case omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_LENS:
+ return "Lens";
case omnibox::
SuggestTemplateInfo_TemplateAction_ActionType_CHROME_TAB_SWITCH:
return "TabSwitch";
@@ -308,6 +310,66 @@
}
}
+TEST_F(OmniboxActionInSuggestTest, Create_ValidatesUriSchemes) {
+ // CALL actions require tel: scheme.
+ {
+ TemplateAction action;
+ action.set_action_type(
+ omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CALL);
+ action.set_action_uri("tel:1234567890");
+ EXPECT_NE(nullptr, OmniboxActionInSuggest::Create(action, std::nullopt));
+
+ action.set_action_uri("https://google.com");
+ EXPECT_EQ(nullptr, OmniboxActionInSuggest::Create(action, std::nullopt));
+
+ action.set_action_uri("javascript:alert(1)");
+ EXPECT_EQ(nullptr, OmniboxActionInSuggest::Create(action, std::nullopt));
+ }
+
+ // Web actions require HTTP or HTTPS scheme.
+ const ActionType web_action_types[] = {
+ omnibox::SuggestTemplateInfo_TemplateAction_ActionType_DIRECTIONS,
+ omnibox::SuggestTemplateInfo_TemplateAction_ActionType_REVIEWS,
+ omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_AIM,
+ omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_LENS,
+ omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_TAB_SWITCH,
+ };
+
+ for (auto type : web_action_types) {
+ TemplateAction action;
+ action.set_action_type(type);
+
+ action.set_action_uri("https://www.google.com");
+ EXPECT_NE(nullptr, OmniboxActionInSuggest::Create(action, std::nullopt))
+ << "Failed for type: " << ToString(type);
+
+ action.set_action_uri("http://www.google.com");
+ EXPECT_NE(nullptr, OmniboxActionInSuggest::Create(action, std::nullopt))
+ << "Failed for type: " << ToString(type);
+
+ action.set_action_uri("javascript:alert(1)");
+ EXPECT_EQ(nullptr, OmniboxActionInSuggest::Create(action, std::nullopt))
+ << "Failed for type: " << ToString(type);
+
+ action.set_action_uri("data:text/html,<h1>XSS</h1>");
+ EXPECT_EQ(nullptr, OmniboxActionInSuggest::Create(action, std::nullopt))
+ << "Failed for type: " << ToString(type);
+
+ action.set_action_uri("tel:1234567890");
+ EXPECT_EQ(nullptr, OmniboxActionInSuggest::Create(action, std::nullopt))
+ << "Failed for type: " << ToString(type);
+ }
+
+ // Empty URI is allowed (e.g. for actions with search parameters).
+ {
+ TemplateAction action;
+ action.set_action_type(
+ omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_AIM);
+ action.set_action_uri("");
+ EXPECT_NE(nullptr, OmniboxActionInSuggest::Create(action, std::nullopt));
+ }
+}
+
#if BUILDFLAG(IS_ANDROID)
TEST_F(OmniboxActionInSuggestTest, ShowAsActionButtonForTabSwitch) {
TemplateAction template_action;
Original Bug Report
Potential Universal XSS in Omnibox Actions in Suggest on Android
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential Universal Cross-Site Scripting (UXSS) vulnerability exists in the Omnibox on Android. An attacker-controlled custom search engine can spoof a Google search URL to serve malicious Action in Suggest chips containing “javascript:” URIs, which execute in the context of the user’s active tab when clicked.
Affected files:
components/omnibox/browser/actions/omnibox_action_in_suggest.ccchrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/suggestions/action/OmniboxActionInSuggest.javacomponents/omnibox/browser/base_search_provider.cc
Estimated timestamp from git blame: 2023-05-18
Overview
A potential Universal Cross-Site Scripting (UXSS) vulnerability exists on Android due to missing validation in the Omnibox “Actions in Suggest” feature. An attacker can register a malicious custom search engine via OpenSearch. By setting the Search URL to a legitimate Google domain (e.g., google.com) and the Suggest URL to their own server, they can fool Chrome’s TemplateURLIsGoogle check. This allows the attacker’s suggest server to return SuggestTemplateInfo protobufs containing CHROME_AIM actions with javascript: URIs.
Because IsValidActionURIForType fails to validate the protocol scheme for CHROME_AIM, the malicious chip is displayed in the Omnibox. When the user clicks the action chip, the javascript: URI is passed to LoadUrlParams and navigates the underlying active tab. Since this browser-initiated navigation does not provide an initiator_origin, it bypasses the opaque-origin security checks in NavigationControllerImpl and executes the JavaScript directly in the origin of whatever website was currently open in the active tab.
Potential Steps to Reproduce
(Note: These are potential steps based on code analysis. Our tooling has not executed a working proof of concept.)
- An attacker creates a website serving an OpenSearch Description Document (OSDD) XML.
- In the XML, the
<Url type="text/html" ...>is set tohttps://www.google.com/search?q={searchTerms}, while the<Url type="application/x-suggestions+json" ...>points to the attacker’s server (e.g.,https://attacker.com/suggest?q={searchTerms}). - The user visits the attacker’s website, and Chrome detects and registers the OpenSearch engine. The user either sets it as default or triggers keyword search in the Omnibox.
- When the user types a query,
SearchProviderfetches suggestions from the attacker’s suggest URL. - The attacker’s server returns a JSON response containing a
"google:suggesttemplate"base64-encoded protobuf payload. - The protobuf defines an
action_suggestionslist with aTemplateActionwhereaction_type = CHROME_AIM(1000) andaction_uri = "javascript:alert(document.domain)". BaseSearchProviderallows parsing the action becausesearch::TemplateURLIsGooglereturns true (it only checks the Search URL domain).IsValidActionURIForTypeallows thejavascript:URI because theswitchstatement lacks a case forCHROME_AIMand falls through todefault: return true;.- The Omnibox displays a “Spark/Rainbow” Action Chip. The user clicks the chip while they have a sensitive website open in the active tab (e.g., a banking site).
OmniboxActionInSuggest.javainvokesdelegate.loadPageInCurrentTab(), constructing aLoadUrlParamswith thejavascript:string and a nullinitiator_origin.NavigationControllerImpl::NavigateWithoutEntryreceives the parameters. Becauseparams.initiator_originis absent, the opaque origin blocking check is bypassed. The script executes in the underlying active tab context, bypassing the Same-Origin Policy.
Suggested Fix
The validation function IsValidActionURIForType in components/omnibox/browser/actions/omnibox_action_in_suggest.cc should be updated to enforce strict scheme validation for all action types, and block javascript: execution at a fundamental level.
- Explicitly block all
javascript:schemes early in the function to prevent any fail-open scenarios for new or unhandled action types. - Ensure
CHROME_AIMandCHROME_TAB_SWITCHare explicitly added to theaction_url.SchemeIsHTTPOrHTTPS()case.
bool IsValidActionURIForType(
const std::string& action_uri_str,
omnibox::SuggestTemplateInfo_TemplateAction_ActionType action_type) {
GURL action_url(action_uri_str);
if (!action_url.is_valid()) {
return false;
}
// Block javascript: schemes unconditionally to prevent XSS in the active tab context.
if (action_url.SchemeIs(url::kJavaScriptScheme)) {
return false;
}
switch (action_type) {
case omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CALL:
return action_url.SchemeIs(url::kTelScheme);
case omnibox::SuggestTemplateInfo_TemplateAction_ActionType_DIRECTIONS:
case omnibox::SuggestTemplateInfo_TemplateAction_ActionType_REVIEWS:
case omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_AIM:
case omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_TAB_SWITCH:
return action_url.SchemeIsHTTPOrHTTPS();
default:
return true;
}
}
Evaluated with Chrome root at commit: ae14e9176a277261f3c963589b6b045c43cec1dc
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.