CVE-2026-3939
Overview
Background
- PDF Viewer extension (`extension_misc::kPdfExtensionId`)
- Chrome’s built-in PDF viewer runs as a component extension and uses extension APIs such as
tabs.createandwindows.createto open links from a PDF in a new tab or window. - `NavigateParams`
- The browser-side description of a navigation passed to
Navigate(). Itsis_renderer_initiated,initiator_originandsource_site_instancefields tell the navigation stack whether to apply the stricter checks for content-initiated navigations or treat the navigation as trusted and browser-initiated. - `OpenTabHelper`
- A helper in
chrome/browser/extensions/open_tab_helper.ccthat implements tab opening for extension functions (e.g.tabs.create) and buildsNavigateParams. - Renderer-initiated vs browser-initiated
- Browser-initiated navigations (e.g. typed in the omnibox) get more privilege, such as being allowed to load URLs that web content may not open. Content-originated navigations must be marked renderer-initiated with a correct initiator so those restrictions apply.
Root Cause Analysis
The supplied diff is described by its own commit message as a refactor (“Combine duplicate PDF navigation code … into a common function”). It does not clearly show the advisory’s policy-enforcement fix. Both before and after the patch, when the calling extension is the PDF Viewer, NavigateParams gets is_renderer_initiated = true, initiator_origin = extension->origin() and a source_site_instance created for that origin.
The patch moves this logic into the new static OpenTabHelper::MaybeSetPdfNavigateParams and calls it from OpenTabHelper::OpenTab (same behavior as before) and from the window-creation path in chrome/browser/extensions/api/tabs/tabs_api.cc. The commit message ties that path to WindowsCreateFunction::OnBrowserWindowCreated(), but the enclosing function is not named in the hunk. The one visible behavior change is in that tabs_api.cc path: the old if (PDF) {...} else if (set_self_as_opener_) {...} chain becomes an early return navigate_params; when the helper returns true, so any statements after the set_self_as_opener_ block are now skipped for PDF Viewer navigations. The hunk does not show what those statements are. The underlying invariant, that navigations triggered by PDF content opening new tabs or windows must be treated as renderer-initiated from the PDF extension’s origin and not as privileged browser-initiated navigations, is consistent with the advisory. However, this diff alone does not show which earlier path lacked it, so the connection between this commit and the vulnerability is not established by the diff and may lie in a related CL under bug 40058077.
OpenTabHelper::MaybeSetPdfNavigateParams and makes the window path return right after applying it, but it is mainly a consolidation, not a visibly new check.Attack Path
- Deliver a crafted PDF The attacker hosts a PDF containing a link and the victim opens it in Chrome’s built-in PDF viewer.
- Link opened in new tab/window
The PDF viewer opens the link through the extension tabs/windows APIs (
OpenTabHelper::OpenTabor the window-creation path intabs_api.cc) (inferred: a user click is likely needed to follow the link). - Navigation classified by `NavigateParams`
Whether the target URL is subject to renderer-initiated navigation restrictions depends on the
is_renderer_initiated/initiator_origin/source_site_instancevalues set for PDF Viewer calls. - Policy bypass if misclassified On a path that does not mark the navigation as PDF/renderer-initiated (or that later overrides those fields), the navigation could get browser-initiated privileges and reach URLs web content should not open (inferred; the diff does not show the specific bypassed check).
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/extensions/api/tabs/tabs_api.cc |
modified | |
ifchrome/browser/extensions/open_tab_helper.cc |
modified | |
BrowserWindowInterfacechrome/browser/extensions/open_tab_helper.h |
modified | |
ExtensionFunctionchrome/browser/extensions/open_tab_helper.h |
modified | |
GURLchrome/browser/extensions/open_tab_helper.h |
modified | |
WebContentschrome/browser/extensions/open_tab_helper.h |
modified |
Files Changed
chrome/browser/extensions/api/tabs/tabs_api.ccchrome/browser/extensions/open_tab_helper.ccchrome/browser/extensions/open_tab_helper.h
Audit Directions
- Other PDF-initiated navigation entry pointsCheck every path through which the PDF viewer or its extension APIs navigate (
TabsUpdateFunction::UpdateURL, window creation, downloads,chrome.tabs/chrome.windowscalls, postMessage-driven navigations) and confirm each sets renderer-initiated status, the PDF initiator origin and a matchingsource_site_instance. - Extension API `NavigateParams` constructionAudit extension functions that build
NavigateParamsfor code that leavesis_renderer_initiatedfalse or omitsinitiator_originwhen the request can come from untrusted content. That gives browser-initiated privileges to attacker-controlled URLs. - Fields overwritten after the security-relevant setupWhere
NavigateParamsis filled in stages (as in thetabs_api.ccpath), confirm later code (set_self_as_opener_, opener/BrowsingInstance handling) cannot overwrite the initiator or site instance set for untrusted callers.
Patch
From b6371f6acfdb3ce7dbac92ff054f41123637020f Mon Sep 17 00:00:00 2001
From: Lei Zhang <thestig@chromium.org>
Date: Thu, 29 Jan 2026 14:44:40 -0800
Subject: [PATCH] Refactor common code into OpenTabHelper::MaybeSetPdfNavigateParams()
Combine duplicate PDF navigation code in OpenTabHelper::OpenTab() and
WindowsCreateFunction::OnBrowserWindowCreated() into a common function.
Bug: 40058077
Change-Id: If0de22a2514a4ae79be478e565d34ad36147b8df
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7517501
Reviewed-by: Charlie Reis <creis@chromium.org>
Commit-Queue: Lei Zhang <thestig@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1576873}
---
diff --git a/chrome/browser/extensions/api/tabs/tabs_api.cc b/chrome/browser/extensions/api/tabs/tabs_api.cc
index c8789f35..1eef7ed 100644
--- a/chrome/browser/extensions/api/tabs/tabs_api.cc
+++ b/chrome/browser/extensions/api/tabs/tabs_api.cc
@@ -1027,15 +1027,11 @@
// to be compatible with the navigation capturing behavior.
navigate_params.pwa_navigation_capturing_force_off = true;
- if (extension() && extension()->id() == extension_misc::kPdfExtensionId) {
- // Treat PDF open-in-new-window navigations consistently with other PDF
- // navigations, as done in TabsUpdateFunction::UpdateURL().
- navigate_params.is_renderer_initiated = true;
- navigate_params.initiator_origin = extension()->origin();
- navigate_params.source_site_instance =
- content::SiteInstance::CreateForURL(
- browser_context(), navigate_params.initiator_origin->GetURL());
- } else if (set_self_as_opener_) {
+ if (OpenTabHelper::MaybeSetPdfNavigateParams(*this, navigate_params)) {
+ return navigate_params;
+ }
+
+ if (set_self_as_opener_) {
// Depending on the `setSelfAsOpener` option, we need to put the new
// contents in the same BrowsingInstance as their opener. See also
// https://crbug.com/713888.
diff --git a/chrome/browser/extensions/open_tab_helper.cc b/chrome/browser/extensions/open_tab_helper.cc
index 1f1aaadc..4591ee08 100644
--- a/chrome/browser/extensions/open_tab_helper.cc
+++ b/chrome/browser/extensions/open_tab_helper.cc
@@ -204,14 +204,7 @@
// the navigation capturing behavior.
navigate_params.pwa_navigation_capturing_force_off = true;
- if (extension && extension->id() == extension_misc::kPdfExtensionId) {
- // Treat PDF open-in-new-tab navigations consistently with other PDF
- // navigations, as done in TabsUpdateFunction::UpdateURL().
- navigate_params.is_renderer_initiated = true;
- navigate_params.initiator_origin = extension->origin();
- navigate_params.source_site_instance = content::SiteInstance::CreateForURL(
- function.browser_context(), navigate_params.initiator_origin->GetURL());
- }
+ MaybeSetPdfNavigateParams(function, navigate_params);
base::WeakPtr<content::NavigationHandle> handle = Navigate(&navigate_params);
if (handle && params.bookmark_id) {
@@ -235,4 +228,19 @@
return new_contents;
}
+// static
+bool OpenTabHelper::MaybeSetPdfNavigateParams(const ExtensionFunction& function,
+ NavigateParams& navigate_params) {
+ auto* const extension = function.extension();
+ if (!extension || extension->id() != extension_misc::kPdfExtensionId) {
+ return false;
+ }
+
+ navigate_params.is_renderer_initiated = true;
+ navigate_params.initiator_origin = extension->origin();
+ navigate_params.source_site_instance = content::SiteInstance::CreateForURL(
+ function.browser_context(), navigate_params.initiator_origin->GetURL());
+ return true;
+}
+
} // namespace extensions
diff --git a/chrome/browser/extensions/open_tab_helper.h b/chrome/browser/extensions/open_tab_helper.h
index 3a23f797..05130f8 100644
--- a/chrome/browser/extensions/open_tab_helper.h
+++ b/chrome/browser/extensions/open_tab_helper.h
@@ -14,6 +14,7 @@
class BrowserWindowInterface;
class ExtensionFunction;
class GURL;
+struct NavigateParams;
namespace content {
class WebContents;
@@ -53,6 +54,12 @@
BrowserWindowInterface& browser,
const ExtensionFunction& function,
const Params& params);
+
+ // If `function` is for the PDF Viewer, then mark the PDF-initiated navigation
+ // as renderer-initiated in `navigate_params` and return true. Otherwise
+ // return false and `navigate_params` remains the same.
+ static bool MaybeSetPdfNavigateParams(const ExtensionFunction& function,
+ NavigateParams& navigate_params);
};
} // namespace extensions
Original Bug Report
PDF bookmarks bypass SameSite Strict cookies.
UserAgent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36
Steps to reproduce the problem:
- On the target set a cookie using document.cookie = “A=A;Secure;SameSite=Strict”;
- Create a PDF with a URL bookmark or use https://github.com/chromium/chromium/blob/main/chrome/test/data/pdf/test-bookmarks-with-zoom.pdf
- Click the bookmark on the PDF and notice the cookies gets sent. With the following headers: sec-fetch-site “none” sec-fetch-mode “navigate” sec-fetch-dest “document”
I think because extension APIs are used such as chrome.tabs.create, Restrictions that are normally put on navigation’s get bypassed excluding the schemes since that gets checked well not sure if it gets checked for submitForm but thats POST only.
What is the expected behavior? For it to be like chrome.tabs.update and keep the initiator.
What went wrong? PDF viewer bypassing SameSite Strict cookies and Fetch metadata defenses.
Did this work before? N/A
Chrome version: 96.0.4664.45 Channel: stable OS Version: 10.0