Medium chrome Info Disclosure 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Omnibox
DescriptionInformation leak in Omnibox
ComponentOmnibox
Sub-componentUI>Browser>Omnibox
Bug ClassInfo Disclosure
Tracker501647772
Fix commite823ca74622f (chromium/src) +59/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
if
chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediator.java
modified

Files Changed

  • chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediator.java
  • chrome/android/junit/src/org/chromium/chrome/browser/WebSearchDelegateUnitTest.java
  • chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediatorUnitTest.java
  • content/browser/renderer_host/navigation_controller_impl_unittest.cc
  • chrome/android/java/src/org/chromium/chrome/browser/WebSearchDelegate.java
From e823ca74622f6ed34ef284bfc5b165e7b9b7d025 Mon Sep 17 00:00:00 2001
From: Mike Taylor <miketaylr@chromium.org>
Date: Tue, 08 Sep 2026 17:24:17 -0700
Subject: [PATCH] Remove extra headers on cross-origin redirects in WebSearchDelegate

When constructing LoadUrlParams for web search navigations in
WebSearchDelegate, verbatim request headers are attached to the initial
search URL. By default, LoadUrlParams does not clear these extra headers
upon redirecting across origins.

This change sets setRemoveExtraHeadersOnCrossOriginRedirect(true) on the
created LoadUrlParams object so that extra headers are correctly cleared
if the search request redirects to a cross-origin target.

TAG=agy

Bug: 501647772
Change-Id: If77b7fa7e2d39e7284fc70ce210931260daa65c0
Low-Coverage-Reason: OTHER Coverage not made worse by changes in this CL
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8363224
Commit-Queue: Mike Taylor <miketaylr@chromium.org>
Reviewed-by: Patrick Noland <pnoland@chromium.org>
Reviewed-by: Nasko Oskov <nasko@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1694217}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/WebSearchDelegate.java b/chrome/android/java/src/org/chromium/chrome/browser/WebSearchDelegate.java
index 1451a74..f4a3bda 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/WebSearchDelegate.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/WebSearchDelegate.java
@@ -77,6 +77,7 @@
 
         LoadUrlParams loadUrlParams = new LoadUrlParams(url);
         loadUrlParams.setVerbatimHeaders(headers);
+        loadUrlParams.setRemoveExtraHeadersOnCrossOriginRedirect(true);
         loadUrlParams.setTransitionType(PageTransition.GENERATED);
         return loadUrlParams;
     }
diff --git a/chrome/android/junit/src/org/chromium/chrome/browser/WebSearchDelegateUnitTest.java b/chrome/android/junit/src/org/chromium/chrome/browser/WebSearchDelegateUnitTest.java
index 314ae6b..fc4ae8e 100644
--- a/chrome/android/junit/src/org/chromium/chrome/browser/WebSearchDelegateUnitTest.java
+++ b/chrome/android/junit/src/org/chromium/chrome/browser/WebSearchDelegateUnitTest.java
@@ -4,10 +4,12 @@
 
 package org.chromium.chrome.browser;
 
+import static org.junit.Assert.assertTrue;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -17,6 +19,8 @@
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
 import org.mockito.Mock;
 import org.mockito.junit.MockitoJUnit;
 import org.mockito.junit.MockitoRule;
@@ -32,10 +36,12 @@
 import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactory;
 import org.chromium.chrome.browser.search_engines.TemplateUrlServiceFactoryJni;
 import org.chromium.chrome.browser.tab.Tab;
+import org.chromium.chrome.browser.tab.TabLaunchType;
 import org.chromium.chrome.browser.tabmodel.TabModelSelector;
 import org.chromium.components.feature_engagement.EventConstants;
 import org.chromium.components.feature_engagement.Tracker;
 import org.chromium.components.search_engines.TemplateUrlService;
+import org.chromium.content_public.browser.LoadUrlParams;
 import org.chromium.content_public.browser.WebContents;
 
 /** Unit tests for {@link WebSearchDelegate}. */
@@ -52,6 +58,7 @@
     @Mock private TemplateUrlService mTemplateUrlService;
     @Mock private DataProtectionBridge.Natives mDataProtectionBridgeMock;
     @Mock private TemplateUrlServiceFactory.Natives mTemplateUrlServiceFactoryMock;
+    @Captor private ArgumentCaptor<LoadUrlParams> mLoadUrlParamsCaptor;
 
     private ActivityTabProvider mActivityTabProvider;
     private SettableMonotonicObservableSupplier<TabModelSelector> mTabModelSelectorSupplier;
@@ -91,11 +98,38 @@
 
         mDelegate.performSearch("query");
 
-        verify(mTabModelSelector).openNewTab(any(), anyInt(), any(), anyBoolean());
+        verify(mTabModelSelector)
+                .openNewTab(
+                        mLoadUrlParamsCaptor.capture(),
+                        eq(TabLaunchType.FROM_LONGPRESS_FOREGROUND),
+                        eq(mTab),
+                        eq(false));
+        assertTrue(mLoadUrlParamsCaptor.getValue().getRemoveExtraHeadersOnCrossOriginRedirect());
         verify(mTracker).notifyEvent(EventConstants.WEB_SEARCH_PERFORMED);
     }
 
     @Test
+    public void testSearch_removesExtraHeadersOnCrossOriginRedirect() {
+        doAnswer(
+                        invocation -> {
+                            ((Runnable) invocation.getArgument(2)).run();
+                            return null;
+                        })
+                .when(mDataProtectionBridgeMock)
+                .shouldAllowSearchWith(anyInt(), any(), any());
+
+        mDelegate.performSearch("sample search");
+
+        verify(mTabModelSelector)
+                .openNewTab(
+                        mLoadUrlParamsCaptor.capture(),
+                        eq(TabLaunchType.FROM_LONGPRESS_FOREGROUND),
+                        eq(mTab),
+                        eq(false));
+        assertTrue(mLoadUrlParamsCaptor.getValue().getRemoveExtraHeadersOnCrossOriginRedirect());
+    }
+
+    @Test
     public void testSearch_NotAllowedByPolicy() {
         // Do not invoke the callback to simulate the policy blocking the action.
         mDelegate.performSearch("query");
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediator.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediator.java
index 04834e8..d1916c7c 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediator.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediator.java
@@ -1173,6 +1173,7 @@
                                     assertNonNull(mProfileSupplier.get()),
                                     mTemplateUrlServiceSupplier.get()));
                 }
+                loadUrlParams.setRemoveExtraHeadersOnCrossOriginRedirect(true);
                 loadUrlParams.setTransitionType(
                         omniboxLoadUrlParams.transitionType | PageTransition.FROM_ADDRESS_BAR);
                 if (omniboxLoadUrlParams.inputStartTimestamp != 0) {
diff --git a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediatorUnitTest.java b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediatorUnitTest.java
index 947a508..aa380980 100644
--- a/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediatorUnitTest.java
+++ b/chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediatorUnitTest.java
@@ -1051,6 +1051,7 @@
         assertEquals(
                 PageTransition.TYPED | PageTransition.FROM_ADDRESS_BAR,
                 mLoadUrlParamsCaptor.getValue().getTransitionType());
+        assertTrue(mLoadUrlParamsCaptor.getValue().getRemoveExtraHeadersOnCrossOriginRedirect());
     }
 
     @Test
@@ -1112,6 +1113,7 @@
         assertEquals(
                 PageTransition.TYPED | PageTransition.FROM_ADDRESS_BAR,
                 mLoadUrlParamsCaptor.getValue().getTransitionType());
+        assertTrue(mLoadUrlParamsCaptor.getValue().getRemoveExtraHeadersOnCrossOriginRedirect());
 
         verify(mTab).addObserver(mTabObserverCaptor.capture());
         mTabObserverCaptor.getValue().onLoadUrl(mTab, mLoadUrlParams, mLoadUrlResult);
@@ -1151,6 +1153,7 @@
         assertEquals(
                 PageTransition.TYPED | PageTransition.FROM_ADDRESS_BAR,
                 mLoadUrlParamsCaptor.getValue().getTransitionType());
+        assertTrue(mLoadUrlParamsCaptor.getValue().getRemoveExtraHeadersOnCrossOriginRedirect());
         assertTrue(mLoadUrlParamsCaptor.getValue().getVerbatimHeaders().contains(text));
         assertEquals(data, mLoadUrlParamsCaptor.getValue().getPostData().getEncodedNativeForm());
     }
@@ -1175,6 +1178,7 @@
         assertEquals(
                 PageTransition.TYPED | PageTransition.FROM_ADDRESS_BAR,
                 mLoadUrlParamsCaptor.getValue().getTransitionType());
+        assertTrue(mLoadUrlParamsCaptor.getValue().getRemoveExtraHeadersOnCrossOriginRedirect());
         String verbatimHeaders = mLoadUrlParamsCaptor.getValue().getVerbatimHeaders();
         assertTrue(verbatimHeaders.contains("Authorization: Bearer token123"));
         assertTrue(verbatimHeaders.contains("Custom-Header: custom-value"));
@@ -1242,6 +1246,7 @@
         assertEquals(
                 PageTransition.TYPED | PageTransition.FROM_ADDRESS_BAR,
                 mLoadUrlParamsCaptor.getValue().getTransitionType());
+        assertTrue(mLoadUrlParamsCaptor.getValue().getRemoveExtraHeadersOnCrossOriginRedirect());
     }
 
     @Test
@@ -1264,6 +1269,22 @@
         assertEquals(
                 PageTransition.TYPED | PageTransition.FROM_ADDRESS_BAR,
                 mLoadUrlParamsCaptor.getValue().getTransitionType());
+        assertTrue(mLoadUrlParamsCaptor.getValue().getRemoveExtraHeadersOnCrossOriginRedirect());
+    }
+
+    @Test
+    public void testLoadUrl_removesExtraHeadersOnCrossOriginRedirect() {
+        mMediator.onFinishNativeInitialization();
+        mProfileSupplier.set(mProfile);
+
+        doReturn(mTab).when(mLocationBarDataProvider).getTab();
+        mMediator.loadUrl(
+                new OmniboxLoadUrlParams.Builder(TEST_URL, PageTransition.TYPED)
+                        .setOpenInNewTab(false)
+                        .build());
+
+        verify(mTab).loadUrl(mLoadUrlParamsCaptor.capture());
+        assertTrue(mLoadUrlParamsCaptor.getValue().getRemoveExtraHeadersOnCrossOriginRedirect());
     }
 
     @Test
diff --git a/content/browser/renderer_host/navigation_controller_impl_unittest.cc b/content/browser/renderer_host/navigation_controller_impl_unittest.cc
index 1d8cc094..87e7b212 100644
--- a/content/browser/renderer_host/navigation_controller_impl_unittest.cc
+++ b/content/browser/renderer_host/navigation_controller_impl_unittest.cc
@@ -842,7 +842,7 @@
   NavigationControllerImpl& controller = controller_impl();
   const GURL url1("http://foo1.com/foo");
   const GURL url2("http://foo2.com/bar");
-  const std::string kExtraHeaders = "Foo: Bar\nBaz: Qux";
+  const std::string kExtraHeaders = "Foo: Bar\nBaz: Qux\nX-Geo: w CgwI";
   std::string kExtraHeadersCRLF;
   base::ReplaceChars(kExtraHeaders, "\n", "\r\n", &kExtraHeadersCRLF);
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/renderer_host/navigation_controller_impl_unittest.cc b/content/browser/renderer_host/navigation_controller_impl_unittest.cc
index 1d8cc094..87e7b212 100644
--- a/content/browser/renderer_host/navigation_controller_impl_unittest.cc
+++ b/content/browser/renderer_host/navigation_controller_impl_unittest.cc
@@ -842,7 +842,7 @@
   NavigationControllerImpl& controller = controller_impl();
   const GURL url1("http://foo1.com/foo");
   const GURL url2("http://foo2.com/bar");
-  const std::string kExtraHeaders = "Foo: Bar\nBaz: Qux";
+  const std::string kExtraHeaders = "Foo: Bar\nBaz: Qux\nX-Geo: w CgwI";
   std::string kExtraHeadersCRLF;
   base::ReplaceChars(kExtraHeaders, "\n", "\r\n", &kExtraHeadersCRLF);
Loading diff…

Original Bug Report

reported by vm...@google.com

Privacy leak: X-Geo header sent to cross-origin redirects from Android omnibox

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 without the Chrome Security team.

Overview: Chrome for Android attaches the X-Geo header, containing precise GPS coordinates, to omnibox searches for authorized Default Search Engines. However, the browser fails to set the remove_extra_headers_on_cross_origin_redirect flag for these navigations. Consequently, if the search engine redirects the user to a third-party site, the sensitive location data is leaked to an origin that was never granted Geolocation permissions.

Affected files:

  • chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediator.java
  • content/browser/renderer_host/navigation_request.cc
  • chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/geo/GeolocationHeader.java
  • chrome/android/java/src/org/chromium/chrome/browser/ui/RootUiCoordinator.java
  • chrome/common/google_url_loader_throttle.cc

Estimated timestamp from git blame: 2026-02-05

Description

On Chrome for Android, search navigations initiated from the omnibox attach an X-Geo header to the initial request if the destination is the Default Search Engine (DSE) and the user has granted it location permissions. This header contains a base64-encoded protocol buffer with the device’s precise latitude, longitude, and accuracy information.

While the browser authorizes the inclusion of this header for the initial request, it fails to prevent the header from being leaked during cross-origin redirects. Specifically, the LoadUrlParams object created in LocationBarMediator.java to initiate the navigation does not have the mRemoveExtraHeadersOnCrossOriginRedirect flag enabled.

Consequently, when the native navigation stack (NavigationRequest::OnRedirectChecksComplete in navigation_request.cc) processes a redirect, it evaluates remove_extra_headers_on_cross_origin_redirect_. Since this defaults to false, the extra headers (including X-Geo) are not stripped. The Chromium network service preserves custom headers by default during redirects, resulting in the sensitive location data being sent to the third-party redirect target.

This bypasses the origin-scoped permission model, allowing an attacker-controlled DSE, or a DSE utilizing open redirects (like DuckDuckGo’s ‘!bang’ syntax), to leak a user’s precise location to arbitrary cross-origin sites.

Technical Details & Code Trace

Based on code analysis, the potential vulnerability occurs via the following path:

  1. In chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediator.java (line ~832), a LoadUrlParams object is created for the omnibox search.
  2. GeolocationHeader.getGeoHeader() is called to retrieve the precise location and format it as the X-Geo string.
  3. The header is attached via loadUrlParams.setVerbatimHeaders() (line ~860).
  4. Crucially, LocationBarMediator never calls loadUrlParams.setRemoveExtraHeadersOnCrossOriginRedirect(true). The flag defaults to false.
  5. The LoadUrlParams are passed across JNI. In content/browser/renderer_host/navigation_controller_impl.cc, the NavigationEntry is initialized with remove_extra_headers_on_cross_origin_redirect set to false.
  6. When the DSE responds with a 302 cross-origin redirect, NavigationRequest::OnRedirectChecksComplete evaluates if (remove_extra_headers_on_cross_origin_redirect_ && !IsSameOrigin(...)).
  7. Because the flag is false, the check fails. The X-Geo header is not added to the removed_headers list passed to the network service, causing it to be forwarded to the cross-origin target.

Note: These steps are based on static code analysis by an automated tooling agent; a live proof-of-concept has not been executed.

Potential Reproduction Steps

  1. On an Android device, install Chrome and grant the application-level Location permission.
  2. Set DuckDuckGo as the Default Search Engine via Settings > Search engine.
  3. Visit https://duckduckgo.com once and grant the Geolocation site permission when prompted.
  4. Tap the omnibox and type a query that triggers a cross-origin redirect, such as !a shoes (which redirects to Amazon).
  5. Observe (via a proxy like Burp Suite or net-export) that the request to the redirect target (www.amazon.com) carries the X-Geo header, despite that origin never having been granted location permissions.

Suggested Fix

In chrome/browser/ui/android/omnibox/java/src/org/chromium/chrome/browser/omnibox/LocationBarMediator.java, explicitly enable the flag to strip extra headers on cross-origin redirects before initiating the load:

loadUrlParams.setRemoveExtraHeadersOnCrossOriginRedirect(true);

This mirrors the protective behavior already implemented in Android WebView (AwContents.java).

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


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
Links in the report