Medium CVSS 9.6 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
9.6
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Save to Drive
DescriptionInsufficient validation of untrusted input in Save to Drive
ComponentSave to Drive
Sub-componentSaveToDrive
Bug ClassInput Validation
Tracker515438919
Fix commit57308d61095c (chromium/src) +41/-2
Related commitsacfd83c6a15d previously mapped (not the fix) · chromium/src
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-21 (per vendor advisory)
Fix landed2026-06-05 (public git)
Disclosed2026-07-29
Patch gap54 days — fix public in source before the advisory
Re-mapped on 2026-10-03: our pipeline had picked the earliest commit citing this bug (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

FunctionChangeNotes
TEST
chrome/browser/save_to_drive/save_to_drive_utils_unittest.cc
modified

Files Changed

  • chrome/browser/save_to_drive/save_to_drive_flow.cc
  • chrome/browser/save_to_drive/save_to_drive_flow_browsertest.cc
  • chrome/browser/save_to_drive/save_to_drive_utils.cc
  • chrome/browser/save_to_drive/save_to_drive_utils.h
  • chrome/browser/save_to_drive/save_to_drive_utils_unittest.cc
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • chrome/browser/save_to_drive/multipart_drive_uploader.cc
  • chrome/browser/save_to_drive/resumable_drive_uploader.cc
  • chrome/browser/save_to_drive/save_to_drive_flow.cc
  • chrome/browser/extensions/api/pdf_viewer_private/pdf_viewer_private_api.cc
  • components/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:

  1. Filename Spoofing: The PDF renderer (plugin) can set the document title by sending a message to the extension frame, which then calls the pdfViewerPrivate.setPdfDocumentTitle API. The browser process implementation in PdfViewerPrivateSetPdfDocumentTitleFunction::Run (chrome/browser/extensions/api/pdf_viewer_private/pdf_viewer_private_api.cc) updates the WebContents title to this renderer-provided string without sanitization or origin validation.

  2. UI Masking: When the ‘Save to Drive’ flow is initiated, the browser displays an AccountChooser dialog (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 from document.pdf to malicious.exe).

  3. Content Control: The browser retrieves the file content from the renderer via the pdf::mojom::SaveDataBufferHandler Mojo interface. The PDFContentReader (chrome/browser/save_to_drive/pdf_content_reader.cc) trusts the total_file_size and the bytes provided by the renderer-hosted Mojo implementation.

  4. Privileged Upload: The SaveToDriveFlow (chrome/browser/save_to_drive/save_to_drive_flow.cc) retrieves the spoofed title from the WebContents and uses it as the filename for the upload. The browser then uses an OAuth2 token with full https://www.googleapis.com/auth/drive scope 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

  1. Compromise a sandboxed PDFium renderer (e.g., via a simulated memory corruption exploit).
  2. From the renderer, send a metadata update to change the document title to payload.exe.
  3. The extension UI will call chrome.pdfViewerPrivate.setPdfDocumentTitle('payload.exe'), updating the browser’s tab title.
  4. The renderer triggers the save flow by calling chrome.pdfViewerPrivate.saveToDrive() (this API does not require a user gesture in the IDL).
  5. The user is presented with the Account Chooser. They see a request to ‘Save to Google Drive’ and select their account.
  6. The browser process fetches the title payload.exe and the malicious bytes from the renderer.
  7. The browser uploads the malicious file to the user’s Google Drive.

Suggested Fix

  1. User Transparency: Modify the AccountChooser UI in chrome/browser/ui/views/save_to_drive/account_chooser_view.cc to explicitly display the filename being saved.
  2. 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 .pdf extension).
  3. 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.

View on issue tracker