CVE-2026-17803
Overview
acfd83c6a15d), which an audit found was a preparatory/helper change, not the fix. This page now points to the commit whose diff removes the bug. No root-cause analysis is published for this CVE.Changed Functions
| Function | Change | Notes |
|---|---|---|
TESTchrome/browser/save_to_drive/save_to_drive_utils_unittest.cc |
modified |
Files Changed
chrome/browser/save_to_drive/save_to_drive_flow.ccchrome/browser/save_to_drive/save_to_drive_flow_browsertest.ccchrome/browser/save_to_drive/save_to_drive_utils.ccchrome/browser/save_to_drive/save_to_drive_utils.hchrome/browser/save_to_drive/save_to_drive_utils_unittest.cc
Patch
diff --git a/chrome/browser/save_to_drive/save_to_drive_flow.cc b/chrome/browser/save_to_drive/save_to_drive_flow.cc
index ed9d57dd..8af8bf1b 100644
--- a/chrome/browser/save_to_drive/save_to_drive_flow.cc
+++ b/chrome/browser/save_to_drive/save_to_drive_flow.cc
@@ -126,7 +126,7 @@
WebContents* contents = GetTabWebContents(&render_frame_host());
CHECK(contents);
- std::u16string title = contents->GetTitle();
+ std::u16string title = EnsurePdfExtension(contents->GetTitle());
upload_title_ = base::UTF16ToUTF8(title);
account_chooser_->GetAccount(contents, title,
base::BindOnce(&SaveToDriveFlow::OnAccountChosen,
diff --git a/chrome/browser/save_to_drive/save_to_drive_utils.cc b/chrome/browser/save_to_drive/save_to_drive_utils.cc
index fa793a46..014a70d6 100644
--- a/chrome/browser/save_to_drive/save_to_drive_utils.cc
+++ b/chrome/browser/save_to_drive/save_to_drive_utils.cc
@@ -6,6 +6,7 @@
#include <string_view>
+#include "base/files/file_path.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/string_view_util.h"
#include "chrome/common/extensions/api/tabs.h"
@@ -47,4 +48,12 @@
return content.starts_with(kPdfMagic);
}
+std::u16string EnsurePdfExtension(const std::u16string& title) {
+ static constexpr base::FilePath::StringViewType kPdfExtensionFilePath =
+ FILE_PATH_LITERAL(".pdf");
+ base::FilePath file_path = base::FilePath::FromUTF16Unsafe(title);
+ file_path = file_path.RemoveExtension().AddExtension(kPdfExtensionFilePath);
+ return file_path.AsUTF16Unsafe();
+}
+
} // namespace save_to_drive
diff --git a/chrome/browser/save_to_drive/save_to_drive_utils.h b/chrome/browser/save_to_drive/save_to_drive_utils.h
index 2ecf284..6fb7376 100644
--- a/chrome/browser/save_to_drive/save_to_drive_utils.h
+++ b/chrome/browser/save_to_drive/save_to_drive_utils.h
@@ -5,6 +5,8 @@
#ifndef CHROME_BROWSER_SAVE_TO_DRIVE_SAVE_TO_DRIVE_UTILS_H_
#define CHROME_BROWSER_SAVE_TO_DRIVE_SAVE_TO_DRIVE_UTILS_H_
+#include <string>
+
#include "base/memory/weak_ptr.h"
#include "mojo/public/cpp/base/big_buffer.h"
@@ -18,6 +20,11 @@
namespace save_to_drive {
+// Returns the sanitized title with `.pdf` extension.
+// 1. Strips away the existing extension (if any).
+// 2. Appends the `.pdf` extension.
+std::u16string EnsurePdfExtension(const std::u16string& title);
+
// Returns the `StreamContainer` weak ptr associated with the given
// `render_frame_host`.
base::WeakPtr<extensions::StreamContainer> GetStreamWeakPtr(
diff --git a/chrome/browser/save_to_drive/save_to_drive_flow_browsertest.cc b/chrome/browser/save_to_drive/save_to_drive_flow_browsertest.cc
index f912176..bdb0d30 100644
--- a/chrome/browser/save_to_drive/save_to_drive_flow_browsertest.cc
+++ b/chrome/browser/save_to_drive/save_to_drive_flow_browsertest.cc
@@ -24,6 +24,7 @@
#include "chrome/browser/save_to_drive/resumable_drive_uploader.h"
#include "chrome/browser/save_to_drive/save_to_drive_event_dispatcher.h"
#include "chrome/browser/save_to_drive/save_to_drive_recorder.h"
+#include "chrome/browser/save_to_drive/save_to_drive_utils.h"
#include "chrome/browser/save_to_drive/time_remaining_calculator.h"
#include "chrome/browser/ui/hats/hats_service_factory.h"
#include "chrome/browser/ui/hats/mock_hats_service.h"
@@ -175,7 +176,8 @@
content::WebContents* web_contents,
const std::u16string& upload_title,
base::OnceCallback<void(std::optional<AccountInfo>)> callback) {
- EXPECT_EQ(upload_title, web_contents->GetTitle());
+ EXPECT_EQ(upload_title,
+ EnsurePdfExtension(web_contents->GetTitle()));
// The callback could kill the flow, which would destroy the
// account chooser. It needs to be reset to avoid a dangling
// pointer.
diff --git a/chrome/browser/save_to_drive/save_to_drive_utils_unittest.cc b/chrome/browser/save_to_drive/save_to_drive_utils_unittest.cc
index 6bdb825..2ce3ee0 100644
--- a/chrome/browser/save_to_drive/save_to_drive_utils_unittest.cc
+++ b/chrome/browser/save_to_drive/save_to_drive_utils_unittest.cc
@@ -50,4 +50,25 @@
EXPECT_FALSE(ValidatePdfMagic(buffer));
}
+TEST(SaveToDriveUtilsTest, EnsurePdfExtension) {
+ // Test case 1: Simple title without extension
+ EXPECT_EQ(EnsurePdfExtension(u"My Document"), u"My Document.pdf");
+
+ // Test case 2: Title with .pdf extension (should keep it)
+ EXPECT_EQ(EnsurePdfExtension(u"My Document.pdf"), u"My Document.pdf");
+
+ // Test case 3: Title with other extension (should replace it)
+ EXPECT_EQ(EnsurePdfExtension(u"My Document.docx"), u"My Document.pdf");
+
+ // Test case 4: Title with multiple extensions (should replace the last one)
+ EXPECT_EQ(EnsurePdfExtension(u"My.Document.docx"), u"My.Document.pdf");
+
+ // Test case 5: Empty title (should return empty string as per FilePath
+ // behavior)
+ EXPECT_EQ(EnsurePdfExtension(u""), u"");
+
+ // Test case 6: Title with spaces and special characters
+ EXPECT_EQ(EnsurePdfExtension(u"My Document (1)"), u"My Document (1).pdf");
+}
+
} // namespace save_to_drive
Regression Test / PoC
diff --git a/chrome/browser/save_to_drive/save_to_drive_flow_browsertest.cc b/chrome/browser/save_to_drive/save_to_drive_flow_browsertest.cc
index f912176..bdb0d30 100644
--- a/chrome/browser/save_to_drive/save_to_drive_flow_browsertest.cc
+++ b/chrome/browser/save_to_drive/save_to_drive_flow_browsertest.cc
@@ -24,6 +24,7 @@
#include "chrome/browser/save_to_drive/resumable_drive_uploader.h"
#include "chrome/browser/save_to_drive/save_to_drive_event_dispatcher.h"
#include "chrome/browser/save_to_drive/save_to_drive_recorder.h"
+#include "chrome/browser/save_to_drive/save_to_drive_utils.h"
#include "chrome/browser/save_to_drive/time_remaining_calculator.h"
#include "chrome/browser/ui/hats/hats_service_factory.h"
#include "chrome/browser/ui/hats/mock_hats_service.h"
@@ -175,7 +176,8 @@
content::WebContents* web_contents,
const std::u16string& upload_title,
base::OnceCallback<void(std::optional<AccountInfo>)> callback) {
- EXPECT_EQ(upload_title, web_contents->GetTitle());
+ EXPECT_EQ(upload_title,
+ EnsurePdfExtension(web_contents->GetTitle()));
// The callback could kill the flow, which would destroy the
// account chooser. It needs to be reset to avoid a dangling
// pointer.
diff --git a/chrome/browser/save_to_drive/save_to_drive_utils_unittest.cc b/chrome/browser/save_to_drive/save_to_drive_utils_unittest.cc
index 6bdb825..2ce3ee0 100644
--- a/chrome/browser/save_to_drive/save_to_drive_utils_unittest.cc
+++ b/chrome/browser/save_to_drive/save_to_drive_utils_unittest.cc
@@ -50,4 +50,25 @@
EXPECT_FALSE(ValidatePdfMagic(buffer));
}
+TEST(SaveToDriveUtilsTest, EnsurePdfExtension) {
+ // Test case 1: Simple title without extension
+ EXPECT_EQ(EnsurePdfExtension(u"My Document"), u"My Document.pdf");
+
+ // Test case 2: Title with .pdf extension (should keep it)
+ EXPECT_EQ(EnsurePdfExtension(u"My Document.pdf"), u"My Document.pdf");
+
+ // Test case 3: Title with other extension (should replace it)
+ EXPECT_EQ(EnsurePdfExtension(u"My Document.docx"), u"My Document.pdf");
+
+ // Test case 4: Title with multiple extensions (should replace the last one)
+ EXPECT_EQ(EnsurePdfExtension(u"My.Document.docx"), u"My.Document.pdf");
+
+ // Test case 5: Empty title (should return empty string as per FilePath
+ // behavior)
+ EXPECT_EQ(EnsurePdfExtension(u""), u"");
+
+ // Test case 6: Title with spaces and special characters
+ EXPECT_EQ(EnsurePdfExtension(u"My Document (1)"), u"My Document (1).pdf");
+}
+
} // namespace save_to_drive
Original Bug Report
Arbitrary Google Drive Write via Compromised PDFium Renderer
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A logic vulnerability in Chrome’s ‘Save to Drive’ feature allows a compromised PDFium renderer to perform authenticated writes of arbitrary data to a user’s Google Drive. The browser process trusts renderer-provided metadata and file content during the upload flow, bypassing intended sandbox restrictions. The attack is facilitated by the lack of filename visibility in the ‘Save to Drive’ user interface.
Affected files:
chrome/browser/save_to_drive/pdf_content_reader.ccchrome/browser/save_to_drive/multipart_drive_uploader.ccchrome/browser/save_to_drive/resumable_drive_uploader.ccchrome/browser/save_to_drive/save_to_drive_flow.ccchrome/browser/extensions/api/pdf_viewer_private/pdf_viewer_private_api.cccomponents/pdf/browser/pdf_document_helper.cc
Estimated timestamp from git blame: 2025-09-08
Summary
A logic vulnerability has been identified in the Chrome ‘Save to Drive’ flow that allows a compromised, sandboxed PDFium renderer process to write arbitrary files to a user’s Google Drive. The browser process acts as a confused deputy, using its privileged OAuth credentials to upload data and metadata provided by the untrusted renderer without sufficient validation or user transparency.
Technical Analysis
When a user interacts with a PDF, the sandboxed renderer can influence the ‘Save to Drive’ process through several vectors:
-
Filename Spoofing: The PDF renderer (plugin) can set the document title by sending a message to the extension frame, which then calls the
pdfViewerPrivate.setPdfDocumentTitleAPI. The browser process implementation inPdfViewerPrivateSetPdfDocumentTitleFunction::Run(chrome/browser/extensions/api/pdf_viewer_private/pdf_viewer_private_api.cc) updates theWebContentstitle to this renderer-provided string without sanitization or origin validation. -
UI Masking: When the ‘Save to Drive’ flow is initiated, the browser displays an
AccountChooserdialog (chrome/browser/ui/views/save_to_drive/account_chooser_view.cc). This dialog displays generic text (e.g., ‘Save to Google Drive’) but does not show the filename to the user. This prevents the user from noticing if the filename has been spoofed (e.g., changed fromdocument.pdftomalicious.exe). -
Content Control: The browser retrieves the file content from the renderer via the
pdf::mojom::SaveDataBufferHandlerMojo interface. ThePDFContentReader(chrome/browser/save_to_drive/pdf_content_reader.cc) trusts thetotal_file_sizeand the bytes provided by the renderer-hosted Mojo implementation. -
Privileged Upload: The
SaveToDriveFlow(chrome/browser/save_to_drive/save_to_drive_flow.cc) retrieves the spoofed title from theWebContentsand uses it as the filename for the upload. The browser then uses an OAuth2 token with fullhttps://www.googleapis.com/auth/drivescope to perform the upload via the browser-process URLLoaderFactory.
Because the PDFium renderer is normally denied network and filesystem access, the ability to perform authenticated writes to the user’s cloud storage represents a significant bypass of sandbox isolation.
Potential Impact
An attacker who has already compromised the PDFium renderer can place arbitrary files into a user’s Google Drive. This could be used to deliver malicious executables, plant spoofed documents, or exfiltrate data by masquerading as a legitimate ‘Save to Drive’ operation. While the attack requires the user to initiate the save and select an account, the lack of filename visibility makes the operation effectively silent.
Potential Reproduction Steps
- Compromise a sandboxed PDFium renderer (e.g., via a simulated memory corruption exploit).
- From the renderer, send a metadata update to change the document title to
payload.exe. - The extension UI will call
chrome.pdfViewerPrivate.setPdfDocumentTitle('payload.exe'), updating the browser’s tab title. - The renderer triggers the save flow by calling
chrome.pdfViewerPrivate.saveToDrive()(this API does not require a user gesture in the IDL). - The user is presented with the Account Chooser. They see a request to ‘Save to Google Drive’ and select their account.
- The browser process fetches the title
payload.exeand the malicious bytes from the renderer. - The browser uploads the malicious file to the user’s Google Drive.
Suggested Fix
- User Transparency: Modify the
AccountChooserUI inchrome/browser/ui/views/save_to_drive/account_chooser_view.ccto explicitly display the filename being saved. - Metadata Validation: Sanitize and validate the filename in the browser process before using it in the Drive API request. Ensure the extension is consistent with the expected file type (e.g., enforce a
.pdfextension). - Content Verification: Perform basic validation on the data returned by the
SaveDataBufferHandler(e.g., verify the PDF magic header) to ensure the renderer is not providing entirely different file types.
Evaluated with Chrome root at commit: 29093e11cf509e3593f6229e4b1b075cca356049
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.