CVE-2026-79124
Overview
Background
- Referrer Policy
- a per-navigation setting (
network.mojom.ReferrerPolicy, e.g.DEFAULT,ORIGIN,ALWAYS,no-referrer) that controls how much of the referring URL is exposed to the destination during a navigation. - `ExternalNavigationParams`
- the Java parameter object that carries a candidate navigation (URL, referrer, transition, etc.) into Chrome’s external-intent handling on Android.
- Main-frame clobbering
- when external-intent handling cancels the intent and instead navigates (“clobbers”) the current main frame to a fallback URL, issuing a fresh
LoadUrlParams. - `NavigationHandle` (Java)
- the Java mirror of the C++ navigation object, populated over JNI from
navigation_handle_proxy.cc.
Root Cause Analysis
When the C++ NavigationHandle referrer was marshalled to Java over JNI, only the referrer URL was passed and the referrer policy was dropped, so Java code only ever saw the URL. In InterceptNavigationDelegateImpl, the fallback/clobber path that builds a new LoadUrlParams reconstructed a Referrer by hardcoding ReferrerPolicy.ALWAYS, forcing the full referrer URL to be attached regardless of the page’s actual, more-restrictive policy (e.g. origin, strict-origin, or no-referrer). This violated the invariant that a navigation must honor the originating document’s referrer policy, causing the full referrer URL to be disclosed to the fallback destination when the original policy would have stripped or reduced it.
The fix carries the policy end-to-end: navigation_handle_proxy.cc now passes GetReferrer().policy over JNI, NavigationHandle.java stores mReferrerPolicy, ExternalNavigationParams holds a full Referrer (URL + policy) instead of a bare GURL, and the clobber path in InterceptNavigationDelegateImpl uses params.getReferrer() (preserving the real policy) instead of synthesizing ReferrerPolicy.ALWAYS. The new test testFallbackUrl_WithReferrerPolicy asserts a non-default policy (ReferrerPolicy.ORIGIN) survives into the clobbering referrer.
ReferrerPolicy.ALWAYS during main-frame clobbering; the fix threads the actual policy through the Referrer object so the intended, potentially restrictive policy is honored.Attack Path
- Serve a page with a restrictive referrer policy
A document (e.g.
originorno-referrer) contains a link that resolves to an intent/external navigation with a fallback URL. - Trigger external-intent handling
The navigation enters
InterceptNavigationDelegateImpl; the referrer URL is present Java-side but its policy has been dropped over JNI (ReferrerPolicynever transmitted pre-patch). - Fallback clobbers the main frame
On the fallback path, the pre-patch code builds
new Referrer(params.getReferrerUrl().getSpec(), ReferrerPolicy.ALWAYS), attaching the full referrer URL to the fallback navigation irrespective of the origin document’s policy. - Referrer leaks to the destination The fallback URL’s server (potentially attacker-controlled) receives the complete referrer URL that the page’s referrer policy was meant to withhold or trim, disclosing sensitive path/query information.
Impact Assessment
ALWAYS referrer policy. The vendor advisory rates this low severity and does not report in-the-wild exploitation; NVD scores it CVSS 6.5.Files Changed
content/public/android/java/src/org/chromium/content_public/browser/NavigationHandle.javacontent/public/android/BUILD.gncontent/browser/android/navigation_handle_proxy.cccomponents/external_intents/android/javatests/src/org/chromium/components/external_intents/ExternalNavigationHandlerTest.javachrome/android/java/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchManager.javacomponents/external_intents/android/BUILD.gncomponents/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationParams.javachrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabExternalNavigationTest.javacomponents/external_intents/android/java/src/org/chromium/components/external_intents/InterceptNavigationDelegateImpl.java
Audit Directions
- Policy dropped at language/process boundariesAudit other JNI and mojo marshalling of
Referrer/navigation metadata for fields (policy, SameSite, sec-metadata) silently omitted so only the URL survives. - Hardcoded `ReferrerPolicy.ALWAYS`Grep for construction of
Referrerwith a fixedReferrerPolicy.ALWAYS(or equivalent) on navigation/redirect/clobber paths, which forces full-URL disclosure regardless of the origin document’s policy. - Fallback/clobber navigationsReview fallback and redirect-handling code that rebuilds
LoadUrlParamsfrom partial navigation state, checking that referrer, initiator origin, and policy are carried faithfully rather than reconstructed with permissive defaults.
Patch
From c9b35fcba98cc9986bdd859b2ea97a2ac9314315 Mon Sep 17 00:00:00 2001
From: Etienne Bergeron <etienneb@google.com>
Date: Fri, 17 Jul 2026 07:40:43 -0700
Subject: [PATCH] Carry Referrer Policy into Java NavigationHandle.
Previously, the referrer policy was dropped when passing the referrer
from C++ to Java NavigationHandle via JNI. This resulted in Java side
only having the referrer URL.
This CL:
1. Updates JNI to pass the referrer policy along with the referrer URL
to Java NavigationHandle.
2. Exposes the referrer policy in Java NavigationHandle.
3. Updates ExternalNavigationParams to carry the Referrer (URL +
policy).
4. Updates InterceptNavigationDelegateImpl to use the preserved referrer
policy when clobbering the main frame, instead of hardcoding
ReferrerPolicy.ALWAYS.
Bug: 497493136
Change-Id: Iad7bf04ab7f861701182e3ff3fcd2cafbbd6d5f0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8092610
Commit-Queue: Etienne Bergeron <etienneb@chromium.org>
Reviewed-by: Michael Thiessen <mthiesse@chromium.org>
Reviewed-by: Arthur Sonzogni <arthursonzogni@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1663892}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchManager.java b/chrome/android/java/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchManager.java
index 5831c3e1..3087848 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchManager.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/contextualsearch/ContextualSearchManager.java
@@ -78,7 +78,9 @@
import org.chromium.content_public.browser.Visibility;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.common.ContentUrlConstants;
+import org.chromium.content_public.common.Referrer;
import org.chromium.net.NetworkChangeNotifier;
+import org.chromium.network.mojom.ReferrerPolicy;
import org.chromium.ui.base.IntentRequestTracker;
import org.chromium.ui.base.LocalizationUtils;
import org.chromium.ui.base.PageTransition;
@@ -1182,7 +1184,14 @@
isRendererInitiated);
ExternalNavigationParams params =
new ExternalNavigationParams.Builder(
- escapedUrl, false, referrerUrl, pageTransition, isRedirect)
+ escapedUrl,
+ false,
+ GURL.isEmptyOrInvalid(referrerUrl)
+ ? null
+ : new Referrer(
+ referrerUrl.getSpec(), ReferrerPolicy.DEFAULT),
+ pageTransition,
+ isRedirect)
.setRedirectHandler(mRedirectHandler)
.setIsMainFrame(isInPrimaryMainFrame)
.build();
diff --git a/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabExternalNavigationTest.java b/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabExternalNavigationTest.java
index f8d1d261..b41b4f2 100644
--- a/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabExternalNavigationTest.java
+++ b/chrome/android/javatests/src/org/chromium/chrome/browser/customtabs/CustomTabExternalNavigationTest.java
@@ -306,7 +306,7 @@
new ExternalNavigationParams.Builder(
insideVerifiedOriginUrl,
/* isIncognito= */ false,
- /* referrer= */ GURL.emptyGURL(),
+ /* referrer= */ null,
/* pageTransition= */ 0,
/* isRedirect= */ false)
.setRedirectHandler(RedirectHandler.create())
@@ -320,7 +320,7 @@
new ExternalNavigationParams.Builder(
outsideVerifiedOriginUrl,
/* isIncognito= */ false,
- /* referrer= */ GURL.emptyGURL(),
+ /* referrer */ null,
/* pageTransition= */ 0,
/* isRedirect= */ false)
.setRedirectHandler(RedirectHandler.create())
@@ -335,7 +335,7 @@
new ExternalNavigationParams.Builder(
insideVerifiedOriginUrl,
/* isIncognito= */ false,
- /* referrer= */ GURL.emptyGURL(),
+ /* referrer= */ null,
/* pageTransition= */ 0,
/* isRedirect= */ false)
.setRedirectHandler(RedirectHandler.create())
diff --git a/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationParams.java b/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationParams.java
index 695d2af..135c2cd 100644
--- a/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationParams.java
+++ b/components/external_intents/android/java/src/org/chromium/components/external_intents/ExternalNavigationParams.java
@@ -12,6 +12,7 @@
import org.chromium.base.RequiredCallback;
import org.chromium.build.annotations.NullMarked;
import org.chromium.build.annotations.Nullable;
+import org.chromium.content_public.common.Referrer;
import org.chromium.ui.base.PageTransition;
import org.chromium.url.GURL;
import org.chromium.url.Origin;
@@ -82,6 +83,7 @@
private final GURL mUrl;
private final boolean mIsIncognito;
+ private final @Nullable Referrer mReferrer;
private final GURL mReferrerUrl;
private final int mPageTransition;
private final boolean mIsRedirect;
@@ -108,7 +110,7 @@
private ExternalNavigationParams(
GURL url,
boolean isIncognito,
- @Nullable GURL referrerUrl,
+ @Nullable Referrer referrer,
int pageTransition,
boolean isRedirect,
RedirectHandler redirectHandler,
@@ -130,7 +132,11 @@
mUrl = url;
mIsIncognito = isIncognito;
mPageTransition = pageTransition;
- mReferrerUrl = (referrerUrl == null) ? GURL.emptyGURL() : referrerUrl;
+ mReferrer = referrer;
+ mReferrerUrl =
+ (mReferrer != null && mReferrer.getUrl() != null)
+ ? new GURL(mReferrer.getUrl())
+ : GURL.emptyGURL();
mIsRedirect = isRedirect;
mRedirectHandler = redirectHandler;
mOpenInNewTab = openInNewTab;
@@ -166,12 +172,23 @@
return mIsIncognito;
}
- /** @return The referrer URL. */
+ /**
+ * @return The referrer URL.
+ */
public GURL getReferrerUrl() {
return mReferrerUrl;
}
- /** @return The page transition for the current navigation. */
+ /**
+ * @return The referrer.
+ */
+ public @Nullable Referrer getReferrer() {
+ return mReferrer;
+ }
+
+ /**
+ * @return The page transition for the current navigation.
+ */
public int getPageTransition() {
return mPageTransition;
}
@@ -289,7 +306,7 @@
public static class Builder {
private final GURL mUrl;
private final boolean mIsIncognito;
- private @Nullable GURL mReferrerUrl;
+ private @Nullable Referrer mReferrer;
private int mPageTransition;
private boolean mIsRedirect;
private @Nullable RedirectHandler mRedirectHandler;
@@ -317,12 +334,12 @@
public Builder(
GURL url,
boolean isIncognito,
- GURL referrer,
+ @Nullable Referrer referrer,
int pageTransition,
boolean isRedirect) {
mUrl = url;
mIsIncognito = isIncognito;
- mReferrerUrl = referrer;
+ mReferrer = referrer;
mPageTransition = pageTransition;
mIsRedirect = isRedirect;
}
@@ -429,7 +446,7 @@
return new ExternalNavigationParams(
mUrl,
mIsIncognito,
- mReferrerUrl,
+ mReferrer,
mPageTransition,
mIsRedirect,
assertNonNull(mRedirectHandler),
diff --git a/components/external_intents/android/java/src/org/chromium/components/external_intents/InterceptNavigationDelegateImpl.java b/components/external_intents/android/java/src/org/chromium/components/external_intents/InterceptNavigationDelegateImpl.java
index 2a26444..c5aa7b0 100644
--- a/components/external_intents/android/java/src/org/chromium/components/external_intents/InterceptNavigationDelegateImpl.java
+++ b/components/external_intents/android/java/src/org/chromium/components/external_intents/InterceptNavigationDelegateImpl.java
@@ -6,6 +6,7 @@
import static org.chromium.build.NullUtil.assumeNonNull;
+import android.text.TextUtils;
import android.util.Pair;
import androidx.annotation.IntDef;
@@ -39,7 +40,6 @@
import org.chromium.content_public.browser.WebContentsObserver;
import org.chromium.content_public.common.ConsoleMessageLevel;
import org.chromium.content_public.common.Referrer;
-import org.chromium.network.mojom.ReferrerPolicy;
import org.chromium.ui.base.PageTransition;
import org.chromium.url.GURL;
import org.chromium.url.Origin;
@@ -283,7 +283,7 @@
navigationHandle.isRedirect(),
navigationHandle.hasUserGesture(),
navigationHandle.isRendererInitiated(),
- navigationHandle.getReferrerUrl(),
+ navigationHandle.getReferrer(),
navigationHandle.isInPrimaryMainFrame(),
navigationHandle.getInitiatorOrigin(),
navigationHandle.isExternalProtocol(),
@@ -409,8 +409,7 @@
isRedirect,
hasUserGesture,
isRendererInitiated,
- GURL.emptyGURL()
- /* referrerUrl= */ ,
+ /* referrer= */ null,
/* isInPrimaryMainFrame= */ false,
initiatorOrigin,
/* isExternalProtocol= */ true,
@@ -443,7 +442,7 @@
boolean isRedirect,
boolean hasUserGesture,
boolean isRendererInitiated,
- GURL referrerUrl,
+ @Nullable Referrer referrer,
boolean isInPrimaryMainFrame,
@Nullable Origin initiatorOrigin,
boolean isExternalProtocol,
@@ -464,7 +463,7 @@
new ExternalNavigationParams.Builder(
escapedUrl,
mClient.isIncognito(),
- referrerUrl,
+ referrer,
pageTransition,
isRedirect)
.setRedirectHandler(redirectHandler)
@@ -771,9 +770,8 @@
int transitionType = PageTransition.LINK;
final LoadUrlParams loadUrlParams = new LoadUrlParams(targetUrl, transitionType);
- if (!params.getReferrerUrl().isEmpty()) {
- Referrer referrer =
- new Referrer(params.getReferrerUrl().getSpec(), ReferrerPolicy.ALWAYS);
+ Referrer referrer = params.getReferrer();
+ if (referrer != null && !TextUtils.isEmpty(referrer.getUrl())) {
loadUrlParams.setReferrer(referrer);
}
// Ideally this navigation would be part of the navigation chain that triggered it and get,
diff --git a/components/external_intents/android/javatests/src/org/chromium/components/external_intents/ExternalNavigationHandlerTest.java b/components/external_intents/android/javatests/src/org/chromium/components/external_intents/ExternalNavigationHandlerTest.java
index 41fbc62..3d05efa 100644
--- a/components/external_intents/android/javatests/src/org/chromium/components/external_intents/ExternalNavigationHandlerTest.java
+++ b/components/external_intents/android/javatests/src/org/chromium/components/external_intents/ExternalNavigationHandlerTest.java
@@ -49,6 +49,7 @@
import org.chromium.base.test.util.Features;
import org.chromium.base.test.util.MaxAndroidSdkLevel;
import org.chromium.base.test.util.MinAndroidSdkLevel;
+import org.chromium.build.annotations.Nullable;
import org.chromium.components.external_intents.ExternalNavigationHandler.IncognitoDialogDelegateWithFallback;
import org.chromium.components.external_intents.ExternalNavigationHandler.OverrideUrlLoadingResult;
import org.chromium.components.external_intents.ExternalNavigationHandler.OverrideUrlLoadingResultType;
@@ -56,6 +57,8 @@
import org.chromium.components.external_intents.ExternalNavigationParams.AsyncActionTakenParams.AsyncActionTakenType;
import org.chromium.content_public.browser.WebContents;
import org.chromium.content_public.browser.test.NativeLibraryTestUtils;
+import org.chromium.content_public.common.Referrer;
+import org.chromium.network.mojom.ReferrerPolicy;
import org.chromium.ui.base.PageTransition;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.ui.modaldialog.DialogDismissalCause;
@@ -1139,6 +1142,24 @@
@Test
@SmallTest
+ public void testFallbackUrl_WithReferrerPolicy() {
+ mDelegate.setCanResolveActivityForExternalSchemes(false);
+
+ // Use a non-default referrer policy.
+ int policy = ReferrerPolicy.ORIGIN;
+ checkUrl(INTENT_URL_WITH_FALLBACK_URL, redirectHandlerForLinkClick())
+ .withReferrer(SEARCH_RESULT_URL_FOR_TOM_HANKS, policy)
+ .expecting(OverrideUrlLoadingResultType.OVERRIDE_WITH_NAVIGATE_TAB, IGNORE);
+
+ Assert.assertNull(mUrlHandler.mStartActivityIntent);
+ Assert.assertEquals(IMDB_WEBPAGE_FOR_TOM_HANKS, mUrlHandler.mNewUrlAfterClobbering);
+ Assert.assertEquals(SEARCH_RESULT_URL_FOR_TOM_HANKS, mUrlHandler.mReferrerUrlForClobbering);
+ Assert.assertNotNull(mUrlHandler.mReferrerForClobbering);
+ Assert.assertEquals(policy, mUrlHandler.mReferrerForClobbering.getPolicy());
+ }
+
+ @Test
+ @SmallTest
public void testFallbackUrl_FallbackToMarketApp() {
mDelegate.setCanResolveActivityForExternalSchemes(false);
@@ -3394,6 +3415,7 @@
public boolean mShouldRequestFileAccess;
public String mNewUrlAfterClobbering;
public String mReferrerUrlForClobbering;
+ public @Nullable Referrer mReferrerForClobbering;
public boolean mRequestFilePermissionsCalled;
public Intent mStartActivityInIncognitoIntent;
public boolean mStartIncognitoIntentCalled;
@@ -3879,6 +3901,7 @@
private final String mUrl;
private String mReferrerUrl;
+ private int mReferrerPolicy = ReferrerPolicy.DEFAULT;
private boolean mIsIncognito;
private int mPageTransition = PageTransition.LINK;
private boolean mIsRedirect;
@@ -3902,6 +3925,12 @@
return this;
}
+ public ExternalNavigationTestParams withReferrer(String referrerUrl, int policy) {
+ mReferrerUrl = referrerUrl;
+ mReferrerPolicy = policy;
+ return this;
+ }
+
public ExternalNavigationTestParams withIsIncognito(boolean isIncognito) {
mIsIncognito = isIncognito;
return this;
@@ -3982,6 +4011,8 @@
mUrlHandler.mNewUrlAfterClobbering = params.targetUrl.getSpec();
mUrlHandler.mReferrerUrlForClobbering =
params.externalNavigationParams.getReferrerUrl().getSpec();
+ mUrlHandler.mReferrerForClobbering =
+ params.externalNavigationParams.getReferrer();
}
}
};
@@ -3989,7 +4020,9 @@
new ExternalNavigationParams.Builder(
new GURL(mUrl),
mIsIncognito,
- new GURL(mReferrerUrl),
+ mReferrerUrl == null
+ ? null
+ : new Referrer(mReferrerUrl, mReferrerPolicy),
mPageTransition,
mIsRedirect)
.setRedirectHandler(mRedirectHandler)
@@ -4010,6 +4043,7 @@
mUrlHandler.mNewUrlAfterClobbering = result.mTargetUrl.getSpec();
mUrlHandler.mReferrerUrlForClobbering =
result.mExternalNavigationParams.getReferrerUrl().getSpec();
+ mUrlHandler.mReferrerForClobbering = result.mExternalNavigationParams.getReferrer();
}
if (result.getResultType() == OverrideUrlLoadingResultType.OVERRIDE_WITH_ASYNC_ACTION) {
mUrlHandler.mAsyncActionCallback = params.getRequiredAsyncActionTakenCallback();
diff --git a/content/browser/android/navigation_handle_proxy.cc b/content/browser/android/navigation_handle_proxy.cc
index 58a4f2d..ae30e5f 100644
--- a/content/browser/android/navigation_handle_proxy.cc
+++ b/content/browser/android/navigation_handle_proxy.cc
@@ -52,6 +52,7 @@
env, java_navigation_handle_,
url::GURLAndroid::FromNativeGURL(
env, cpp_navigation_handle_->GetReferrer().url),
+ static_cast<jint>(cpp_navigation_handle_->GetReferrer().policy),
url::GURLAndroid::FromNativeGURL(
env, cpp_navigation_handle_->GetBaseURLForDataURL()),
cpp_navigation_handle_->IsInPrimaryMainFrame(),
diff --git a/content/public/android/java/src/org/chromium/content_public/browser/NavigationHandle.java b/content/public/android/java/src/org/chromium/content_public/browser/NavigationHandle.java
index c5263d7..d4d8e736 100644
--- a/content/public/android/java/src/org/chromium/content_public/browser/NavigationHandle.java
+++ b/content/public/android/java/src/org/chromium/content_public/browser/NavigationHandle.java
@@ -16,7 +16,9 @@
import org.chromium.build.annotations.NullMarked;
import org.chromium.build.annotations.Nullable;
import org.chromium.content.browser.framehost.PageImpl;
+import org.chromium.content_public.common.Referrer;
import org.chromium.net.NetError;
+import org.chromium.network.mojom.ReferrerPolicy;
import org.chromium.ui.base.PageTransition;
import org.chromium.url.GURL;
import org.chromium.url.Origin;
@@ -35,6 +37,7 @@
private @PageTransition int mPageTransition;
private GURL mUrl;
private @Nullable GURL mReferrerUrl;
+ private int mReferrerPolicy;
private @Nullable GURL mBaseUrlForDataUrl;
private boolean mHasCommitted;
private boolean mIsDownload;
@@ -102,6 +105,7 @@
/* isRestore= */ false);
handle.didStart(
/* referrerUrl= */ GURL.emptyGURL(),
... (file diff truncated)