CVE-2026-3932
Overview
Background
- PDF native page (Android)
- Chrome on Android can show PDFs inline as a native page addressed by an internal URL such as
chrome-native://pdf/link?url=<encoded original URL>;PdfUtils.decodePdfPageUrlrecovers the original URL from it. - PDF re-download
- when a finished navigation lands on such a PDF page URL that
maybeShowNativePagecannot display (e.g. going back to it from a non-PDF page),TabImpl.handleDidFinishNavigationloads the decoded URL again to fetch the PDF. - Browser- vs. renderer-initiated navigation
LoadUrlParamsbuilt with only a URL is treated as browser-initiated with no initiator origin, which carries more trust (for example inSameSite=Strictcookie decisions and scheme restrictions) than a renderer-initiated navigation from a known origin.
Root Cause Analysis
In TabImpl.handleDidFinishNavigation, when maybeShowNativePage returned false, the code called PdfUtils.decodePdfPageUrl(url.getSpec()). Any non-null result was passed to loadUrl(new LoadUrlParams(downloadUrl)). That step had two flaws. First, the decoded URL was not restricted by scheme, so a PDF page URL whose embedded url parameter was something like chrome://version (the new PDF_LINK_ENCODED_INVALID test case) would be loaded. Second, the new load dropped the original navigation’s isRendererInitiated and initiator origin, so it ran as a fresh browser-initiated navigation. Per the commit and the new comment, that could escalate privileges and bypass SameSite=Strict cookie protection. The commit says the re-download can be triggered when the current page is not a PDF, so a page that is not a PDF can drive this path.
The patch makes three changes. It adds PdfUtils.getPdfReDownloadUrl, which returns the decoded URL only if it starts with http:// or https:// and otherwise returns null (so showRenderedPage() runs instead). It adds isRendererInitiated and initiatorOrigin parameters to handleDidFinishNavigation. It copies them onto the re-download LoadUrlParams with setIsRendererInitiated and setInitiatorOrigin. TabWebContentsObserver now passes navigation.isRendererInitiated() and navigation.getInitiatorOrigin().
Attack Path
- Reach a PDF page URL
a navigation in the tab finishes on a
chrome-native://pdf/link?url=...style URL whose embedded target the attacker chose, for example via going back to a PDF URL from a non-PDF page as the commit describes. Exactly how the attacker gets this URL into the navigation is not shown in the diff (inferred). - Native page not shown
maybeShowNativePagereturnsfalse, sohandleDidFinishNavigationdecodes the embedded URL for a re-download. - Laundered navigation
before the patch, the decoded URL (of any scheme, e.g.
chrome://version) was loaded with a bareLoadUrlParams, so it ran as a browser-initiated navigation with no initiator origin. - Policy bypass
this lets web content reach URLs or schemes it could not normally navigate to, and makes a cross-site request look browser-initiated so that
SameSite=Strictcookies are sent. The commit names the cookie bypass; any further consequence on the reached page is (inferred).
Impact Assessment
chrome://, and as a browser-initiated navigation that gets around SameSite=Strict cookie protection. Preconditions: Android only (TabImpl, PdfUtils); the inline PDF path must be active (PdfUtils.shouldOpenPdfInline); and the tab must commit a PDF page URL that maybeShowNativePage won’t show, as with the back-navigation-to-PDF flow the commit describes. Whether this needs a user gesture is not shown. The vendor rates it medium severity (“Insufficient policy enforcement in PDF”).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java |
modified | |
ifchrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.java |
modified |
Files Changed
chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.javachrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsObserver.javachrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.javachrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtilsUnitTest.java
Audit Directions
- Browser-side follow-up navigationslook for other Android/Java code that reacts to a finished or failed navigation by calling
loadUrl(new LoadUrlParams(url)), such as native-page fallbacks, offline pages, download retries or interstitial “try again” actions. Check that those loads keepisRendererInitiated,initiatorOriginand referrer. - Encoded-URL wrappersaudit internal wrapper URLs that embed a target URL in a query parameter (
chrome-native://,chrome://handlers,PdfUtils.encodePdfPageUrlcounterparts). Check that the decoded target is limited by scheme before it is navigated to, opened or downloaded. - History-driven re-executionreview session-history and back/forward restoration paths where a stored entry makes the browser start a new load. Check whether the stored URL or its initiator can be influenced by a page that is not the original initiator.
Patch
From 2265687230f6a3642e766a68d7c3b201ced0bbf6 Mon Sep 17 00:00:00 2001
From: Shu Yang <shuyng@google.com>
Date: Fri, 06 Feb 2026 13:32:39 -0800
Subject: [PATCH] Restrict PDF re-downloads to safe schemes and preserve initiator
Going back to a PDF URL may require a re-download, and this can be
triggered when the current page is not a PDF. Ensure that these
navigations can't elevate their privileges by limiting to HTTP(S)
and preserving information about the initiator.
Bug: 478296121
Change-Id: Ic08a4aa797b0ba11f7010714570e4e9d3be31a48
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7520272
Reviewed-by: Charlie Reis <creis@chromium.org>
Reviewed-by: Sirisha Kavuluru <skavuluru@google.com>
Commit-Queue: Shu Yang <shuyng@google.com>
Cr-Commit-Position: refs/heads/main@{#1581094}
---
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java
index ae0b4dee..95f7a504 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabImpl.java
@@ -112,6 +112,7 @@
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.WindowAndroid;
import org.chromium.url.GURL;
+import org.chromium.url.Origin;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -1854,8 +1855,15 @@
* @param url The URL that was loaded.
* @param transitionType The transition type to the current URL.
* @param isPdf Whether the navigation is for PDF content.
+ * @param isRendererInitiated Whether the navigation is initiated by renderer.
+ * @param initiatorOrigin The Origin that initiated this navigation.
*/
- void handleDidFinishNavigation(GURL url, int transitionType, boolean isPdf) {
+ void handleDidFinishNavigation(
+ GURL url,
+ int transitionType,
+ boolean isPdf,
+ boolean isRendererInitiated,
+ @Nullable Origin initiatorOrigin) {
mIsNativePageCommitPending = false;
boolean isReload = (transitionType & PageTransition.CORE_MASK) == PageTransition.RELOAD;
// Set isPdf param based on the url. This is because the isPdf param in NavigationHandle is
@@ -1866,11 +1874,20 @@
PdfUtils.shouldOpenPdfInline(isIncognito())
&& PdfUtils.isDownloadedPdf(url.getSpec());
if (!maybeShowNativePage(url.getSpec(), isReload, isPdf ? new PdfInfo() : null)) {
- String downloadUrl = PdfUtils.decodePdfPageUrl(url.getSpec());
+ // This is restricted to HTTP(S) URLs specifically, as these are the only schemes that
+ // necessitate a PDF re-download.
+ String downloadUrl = PdfUtils.getPdfReDownloadUrl(url.getSpec());
if (downloadUrl != null) {
- // When the download url is not null, we are on a pdf native page which requires
- // re-download. Load the download url to trigger the re-download.
- loadUrl(new LoadUrlParams(downloadUrl));
+ // When the download url is not null, we are navigating to a pdf native page which
+ // requires re-download. Load the download url to trigger the re-download.
+ var param = new LoadUrlParams(downloadUrl);
+ // To avoid a SameSite=strict cookie bypass, it is important to preserve whether the
+ // navigation was renderer initiated and what its initiator origin was.
+ param.setIsRendererInitiated(isRendererInitiated);
+ if (initiatorOrigin != null) {
+ param.setInitiatorOrigin(initiatorOrigin);
+ }
+ loadUrl(param);
} else {
showRenderedPage();
}
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsObserver.java b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsObserver.java
index eec3439..18b339eb 100644
--- a/chrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsObserver.java
+++ b/chrome/android/java/src/org/chromium/chrome/browser/tab/TabWebContentsObserver.java
@@ -318,7 +318,11 @@
mTab.updateTitle();
mTab.handleDidFinishNavigation(
- navigation.getUrl(), navigation.pageTransition(), navigation.isPdf());
+ navigation.getUrl(),
+ navigation.pageTransition(),
+ navigation.isPdf(),
+ navigation.isRendererInitiated(),
+ navigation.getInitiatorOrigin());
mTab.setIsShowingErrorPage(navigation.isErrorPage());
// TODO(crbug.com/40264745) remove this call. onUrlUpdated should have been called
diff --git a/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.java b/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.java
index 61383fa..222a9473 100644
--- a/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.java
+++ b/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtils.java
@@ -316,6 +316,30 @@
}
/**
+ * Extracts a valid HTTP(S) URL from a PDF page URL for re-downloading.
+ *
+ * <p>This method decodes the provided {@code originalUrl} and verifies that the result uses a
+ * supported scheme (HTTP or HTTPS).
+ *
+ * @param originalUrl The original, potentially encoded, URL string to process.
+ * @return The decoded URL string if it is a valid HTTP(S) URL; {@code null} otherwise.
+ */
+ public static @Nullable String getPdfReDownloadUrl(String originalUrl) {
+ String decodedUrl = decodePdfPageUrl(originalUrl);
+
+ if (decodedUrl == null) {
+ return null;
+ }
+
+ if (decodedUrl.startsWith(UrlConstants.HTTP_URL_PREFIX)
+ || decodedUrl.startsWith(UrlConstants.HTTPS_URL_PREFIX)) {
+ return decodedUrl;
+ }
+
+ return null;
+ }
+
+ /**
* Encode content uri if it is PDF MIME type.
*
* @param uri The uri to be encoded.
diff --git a/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtilsUnitTest.java b/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtilsUnitTest.java
index 91d1c9c..86c1629 100644
--- a/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtilsUnitTest.java
+++ b/chrome/browser/ui/android/pdf/java/src/org/chromium/chrome/browser/pdf/PdfUtilsUnitTest.java
@@ -49,6 +49,8 @@
private static final String PDF_BLOB_URL = "blob:https://www.foo.com/abc";
private static final String PDF_LINK_ENCODED =
"chrome-native://pdf/link?url=https%3A%2F%2Fwww.foo.com%2Ftestfiles%2Fpdf%2Fsample.pdf";
+ private static final String PDF_LINK_ENCODED_INVALID =
+ "chrome-native://pdf/link?url=chrome%3A%2F%2Fversion";
private static final String FILE_PATH = "/media/external/downloads/sample.pdf";
private static final String FILE_NAME = "sample.pdf";
private static final String IMAGE_FILE_URL = "file:///media/external/downloads/sample.jpg";
@@ -238,6 +240,18 @@
}
@Test
+ public void testGetPdfReDownloadUrl_Https() {
+ String downloadUrl = PdfUtils.getPdfReDownloadUrl(PDF_LINK_ENCODED);
+ Assert.assertEquals("The re-download url should match", PDF_LINK, downloadUrl);
+ }
+
+ @Test
+ public void testGetPdfReDownloadUrl_Invalid() {
+ String downloadUrl = PdfUtils.getPdfReDownloadUrl(PDF_LINK_ENCODED_INVALID);
+ Assert.assertNull("The re-download url should be null", downloadUrl);
+ }
+
+ @Test
public void testEncodeDecodeUrlWithSpecialCharacter() {
String encodedUrl = PdfUtils.encodePdfPageUrl(CONTENT_URL_SPECIAL_CHARACTER);
String decodedUrl = PdfUtils.decodePdfPageUrl(encodedUrl);