CVE-2026-17803
Overview
Files Changed
chrome/app/generated_resources.grdchrome/app/generated_resources_grd/IDS_ACCOUNT_CHOOSER_SUBTITLE.png.sha1chrome/browser/save_to_drive/save_to_drive_flow.ccchrome/browser/save_to_drive/save_to_drive_flow.hchrome/browser/save_to_drive/save_to_drive_flow_browsertest.ccchrome/browser/ui/save_to_drive/get_account.hchrome/browser/ui/views/save_to_drive/account_chooser_controller.ccchrome/browser/ui/views/save_to_drive/account_chooser_controller.h
Patch
From acfd83c6a15d997d3f907028014eb5251169eefe Mon Sep 17 00:00:00 2001
From: Yihong Guo <yhguo@google.com>
Date: Fri, 05 Jun 2026 13:15:50 -0700
Subject: [PATCH] [SaveToDrive] Display filename in Account Chooser UI
Fetch the document title in SaveToDriveFlow::Run and pass it to
AccountChooser::GetAccount. Update AccountChooserController and
AccountChooserView to receive and display the filename in the subtitle,
improving transparency for the user. This implements bullet point 1 of
the suggested fix for bug 515438919.
Long name ex: https://screenshot.googleplex.com/3LZT7nWmAy44zxZ.png
Short name ex: https://screenshot.googleplex.com/68MDUzm6pw4UotC.png
Bug: 515438919
Change-Id: I3bbf51f29bff6ed89bfd9dc7a6977abc4da0800d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7886192
Commit-Queue: Yihong Guo <yhguo@google.com>
Reviewed-by: Caroline Rising <corising@chromium.org>
Reviewed-by: Darryl James <dljames@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1642546}
---
diff --git a/chrome/app/generated_resources.grd b/chrome/app/generated_resources.grd
index 446e663..5895fa38 100644
--- a/chrome/app/generated_resources.grd
+++ b/chrome/app/generated_resources.grd
@@ -19540,7 +19540,7 @@
Choose your account
</message>
<message name="IDS_ACCOUNT_CHOOSER_SUBTITLE" desc="Text description for saving files to Drive via Chrome PDF viewer.">
- Find your file in the <ph name="FEATURE_NAME">$1<ex>Saved from Chrome</ex></ph> folder
+ Find your <ph name="FILENAME">$1<ex>document.pdf</ex></ph> in the <ph name="FEATURE_NAME">$2<ex>Saved from Chrome</ex></ph> folder
</message>
<message name="IDS_SAVE_TO_DRIVE_FOLDER_NAME" desc="Name of the folder in Drive where files saved from Chrome are stored." meaning="Name of the folder in Drive where files saved from Chrome are stored. Please use the short version of the branded term for Google Chrome i.e. Chrome">
Saved from Chrome
diff --git a/chrome/app/generated_resources_grd/IDS_ACCOUNT_CHOOSER_SUBTITLE.png.sha1 b/chrome/app/generated_resources_grd/IDS_ACCOUNT_CHOOSER_SUBTITLE.png.sha1
index 1f3e698..4c78f16 100644
--- a/chrome/app/generated_resources_grd/IDS_ACCOUNT_CHOOSER_SUBTITLE.png.sha1
+++ b/chrome/app/generated_resources_grd/IDS_ACCOUNT_CHOOSER_SUBTITLE.png.sha1
@@ -1 +1 @@
-d7d3d3fbe1e8641a34aed3252d3379d0f2fa8a92
\ No newline at end of file
+533d83b42c3b269e2412dae232bb19f33add5c92
\ No newline at end of file
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 da5f8166..ed9d57dd 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,9 @@
WebContents* contents = GetTabWebContents(&render_frame_host());
CHECK(contents);
- account_chooser_->GetAccount(contents,
+ std::u16string title = contents->GetTitle();
+ upload_title_ = base::UTF16ToUTF8(title);
+ account_chooser_->GetAccount(contents, title,
base::BindOnce(&SaveToDriveFlow::OnAccountChosen,
weak_ptr_factory_.GetWeakPtr()));
}
@@ -172,8 +174,6 @@
OnUploadProgress(std::move(progress));
return;
}
- auto* web_contents = WebContents::FromRenderFrameHost(&render_frame_host());
- std::string title = base::UTF16ToUTF8(web_contents->GetTitle());
auto upload_progress_callback = base::BindRepeating(
&SaveToDriveFlow::OnUploadProgress, weak_ptr_factory_.GetWeakPtr());
@@ -182,11 +182,11 @@
if (base::ByteCount(content_reader_->GetSize()) < kMultipartUploadThreshold) {
drive_uploader_ = std::make_unique<MultipartDriveUploader>(
- std::move(title), std::move(account_info),
+ std::move(upload_title_), std::move(account_info),
std::move(upload_progress_callback), profile, content_reader_.get());
} else {
drive_uploader_ = std::make_unique<ResumableDriveUploader>(
- std::move(title), std::move(account_info),
+ std::move(upload_title_), std::move(account_info),
std::move(upload_progress_callback), profile, content_reader_.get());
}
drive_uploader_->Start();
diff --git a/chrome/browser/save_to_drive/save_to_drive_flow.h b/chrome/browser/save_to_drive/save_to_drive_flow.h
index 0b3d9e2..e971904 100644
--- a/chrome/browser/save_to_drive/save_to_drive_flow.h
+++ b/chrome/browser/save_to_drive/save_to_drive_flow.h
@@ -130,6 +130,8 @@
// This is set when an account is chosen.
std::optional<SaveToDriveAccountInfo> save_to_drive_account_info_;
+ // This is the title of the document that is being saved to Drive.
+ std::string upload_title_;
// This is set after the upload starts.
std::optional<extensions::api::pdf_viewer_private::SaveToDriveProgress>
upload_progress_;
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 a3829a5..f912176 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
@@ -6,6 +6,7 @@
#include <memory>
#include <optional>
+#include <string>
#include <utility>
#include "base/functional/callback.h"
@@ -65,6 +66,7 @@
MOCK_METHOD(void,
GetAccount,
(content::WebContents * web_contents,
+ const std::u16string& upload_title,
base::OnceCallback<void(std::optional<AccountInfo>)>
on_account_selected_callback),
(override));
@@ -171,7 +173,9 @@
.WillOnce(
[account_info = std::move(account_info), this](
content::WebContents* web_contents,
+ const std::u16string& upload_title,
base::OnceCallback<void(std::optional<AccountInfo>)> callback) {
+ EXPECT_EQ(upload_title, 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/ui/save_to_drive/get_account.h b/chrome/browser/ui/save_to_drive/get_account.h
index 28cb1a2..9559020 100644
--- a/chrome/browser/ui/save_to_drive/get_account.h
+++ b/chrome/browser/ui/save_to_drive/get_account.h
@@ -7,6 +7,7 @@
#include <memory>
#include <optional>
+#include <string>
#include "base/functional/callback_forward.h"
@@ -30,6 +31,7 @@
// account has been chosen. If the account chooser is canceled, the callback
// will be called with a `std::nullopt`.
virtual void GetAccount(content::WebContents* web_contents,
+ const std::u16string& upload_title,
base::OnceCallback<void(std::optional<AccountInfo>)>
on_account_chosen_callback);
diff --git a/chrome/browser/ui/views/save_to_drive/account_chooser_controller.cc b/chrome/browser/ui/views/save_to_drive/account_chooser_controller.cc
index 2e8664d..c659a81 100644
--- a/chrome/browser/ui/views/save_to_drive/account_chooser_controller.cc
+++ b/chrome/browser/ui/views/save_to_drive/account_chooser_controller.cc
@@ -90,9 +90,11 @@
AccountChooserController::AccountChooserController(
content::WebContents* web_contents,
- signin::IdentityManager* identity_manager)
+ signin::IdentityManager* identity_manager,
+ const std::u16string& upload_title)
: tab_(tabs::TabInterface::MaybeGetFromContents(web_contents)),
identity_manager_(identity_manager),
+ upload_title_(upload_title),
add_account_popup_observer_(
std::make_unique<AddAccountPopupObserver>(this)) {
CHECK(identity_manager_);
@@ -170,7 +172,8 @@
}
std::unique_ptr<AccountChooserView> account_chooser_view =
std::make_unique<AccountChooserView>(this, profile_info.accounts,
- profile_info.primary_account_id);
+ profile_info.primary_account_id,
+ upload_title_);
account_chooser_view_ = account_chooser_view.get();
account_chooser_dialog_delegate_ =
diff --git a/chrome/browser/ui/views/save_to_drive/account_chooser_controller.h b/chrome/browser/ui/views/save_to_drive/account_chooser_controller.h
index 9c6978d1..de54d84 100644
--- a/chrome/browser/ui/views/save_to_drive/account_chooser_controller.h
+++ b/chrome/browser/ui/views/save_to_drive/account_chooser_controller.h
@@ -6,6 +6,7 @@
#define CHROME_BROWSER_UI_VIEWS_SAVE_TO_DRIVE_ACCOUNT_CHOOSER_CONTROLLER_H_
#include <optional>
+#include <string>
#include <vector>
#include "base/functional/callback_forward.h"
@@ -37,7 +38,8 @@
public AccountChooserViewDelegate {
public:
AccountChooserController(content::WebContents* web_contents,
- signin::IdentityManager* identity_manager);
+ signin::IdentityManager* identity_manager,
+ const std::u16string& upload_title);
AccountChooserController(const AccountChooserController&) = delete;
AccountChooserController& operator=(const AccountChooserController&) = delete;
~AccountChooserController() override;
@@ -118,6 +120,7 @@
std::unique_ptr<views::DialogDelegate> account_chooser_dialog_delegate_;
std::unique_ptr<views::Widget> account_chooser_widget_;
std::optional<AccountInfo> selected_account_;
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 a3829a5..f912176 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
@@ -6,6 +6,7 @@
#include <memory>
#include <optional>
+#include <string>
#include <utility>
#include "base/functional/callback.h"
@@ -65,6 +66,7 @@
MOCK_METHOD(void,
GetAccount,
(content::WebContents * web_contents,
+ const std::u16string& upload_title,
base::OnceCallback<void(std::optional<AccountInfo>)>
on_account_selected_callback),
(override));
@@ -171,7 +173,9 @@
.WillOnce(
[account_info = std::move(account_info), this](
content::WebContents* web_contents,
+ const std::u16string& upload_title,
base::OnceCallback<void(std::optional<AccountInfo>)> callback) {
+ EXPECT_EQ(upload_title, 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/ui/views/save_to_drive/account_chooser_controller_interactive_uitest.cc b/chrome/browser/ui/views/save_to_drive/account_chooser_controller_interactive_uitest.cc
index 5134718..e7b5db52 100644
--- a/chrome/browser/ui/views/save_to_drive/account_chooser_controller_interactive_uitest.cc
+++ b/chrome/browser/ui/views/save_to_drive/account_chooser_controller_interactive_uitest.cc
@@ -31,6 +31,7 @@
using ::save_to_drive::testing::GetTestAccounts;
constexpr char kAvatarUrl[] = "https://avatar.com/avatar.png";
+constexpr char16_t kTestUploadTitle[] = u"test.pdf";
AccountChosenCallback GetOnAccountChosenCallback(
const AccountInfo& expected_account,
@@ -91,7 +92,8 @@
account_chooser_controller_ = std::make_unique<AccountChooserController>(
browser()->tab_strip_model()->GetActiveWebContents(),
identity_test_environment_adaptor_->identity_test_env()
- ->identity_manager());
+ ->identity_manager(),
+ kTestUploadTitle);
});
}
diff --git a/chrome/browser/ui/views/save_to_drive/account_chooser_view_unittest.cc b/chrome/browser/ui/views/save_to_drive/account_chooser_view_unittest.cc
index a5adae8..a2e4b7f 100644
--- a/chrome/browser/ui/views/save_to_drive/account_chooser_view_unittest.cc
+++ b/chrome/browser/ui/views/save_to_drive/account_chooser_view_unittest.cc
@@ -4,6 +4,7 @@
#include "chrome/browser/ui/views/save_to_drive/account_chooser_view.h"
+#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/save_to_drive/account_chooser_radio_group_view.h"
#include "chrome/browser/ui/views/save_to_drive/account_chooser_test_util.h"
@@ -28,6 +29,7 @@
namespace {
const char kTestDomain[] = "test.com";
+const char16_t kTestUploadTitle[] = u"test_file.pdf";
using ::save_to_drive::testing::GetTestAccounts;
using ::save_to_drive::testing::VerifyAccountChooserRow;
@@ -42,6 +44,8 @@
static_cast<views::StyledLabel*>(header_view->children().back());
// Verify the subtitle label.
ASSERT_TRUE(subtitle_label);
+ EXPECT_THAT(base::UTF16ToUTF8(subtitle_label->GetText()),
+ ::testing::HasSubstr(base::UTF16ToUTF8(kTestUploadTitle)));
}
void VerifyAccountChooserViewFooter(views::View* footer_view) {
@@ -135,7 +139,8 @@
std::vector<AccountInfo> accounts = GetTestAccounts({"pothos"}, kTestDomain);
AccountChooserView* account_chooser_view =
anchor_view_->AddChildView(std::make_unique<AccountChooserView>(
- &mock_account_chooser_view_delegate_, accounts, std::nullopt));
+ &mock_account_chooser_view_delegate_, accounts, std::nullopt,
+ kTestUploadTitle));
TestSingleAccount(account_chooser_view, accounts.front());
}
@@ -144,7 +149,8 @@
GetTestAccounts({"pothos", "fern"}, kTestDomain);
AccountChooserView* account_chooser_view =
anchor_view_->AddChildView(std::make_unique<AccountChooserView>(
- &mock_account_chooser_view_delegate_, accounts, std::nullopt));
+ &mock_account_chooser_view_delegate_, accounts, std::nullopt,
+ kTestUploadTitle));
TestMultiAccount(account_chooser_view, accounts);
}
@@ -152,7 +158,8 @@
std::vector<AccountInfo> accounts = GetTestAccounts({"pothos"}, kTestDomain);
AccountChooserView* account_chooser_view =
anchor_view_->AddChildView(std::make_unique<AccountChooserView>(
- &mock_account_chooser_view_delegate_, accounts, std::nullopt));
+ &mock_account_chooser_view_delegate_, accounts, std::nullopt,
+ kTestUploadTitle));
TestSingleAccount(account_chooser_view, accounts.front());
std::vector<AccountInfo> new_accounts =
GetTestAccounts({"pothos", "fern"}, kTestDomain);
@@ -165,7 +172,8 @@
GetTestAccounts({"pothos", "fern"}, kTestDomain);
AccountChooserView* account_chooser_view =
anchor_view_->AddChildView(std::make_unique<AccountChooserView>(
- &mock_account_chooser_view_delegate_, accounts, std::nullopt));
+ &mock_account_chooser_view_delegate_, accounts, std::nullopt,
+ kTestUploadTitle));
TestMultiAccount(account_chooser_view, accounts);
std::vector<AccountInfo> new_accounts =
GetTestAccounts({"pothos"}, kTestDomain);
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.