High chrome Authorization Error 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMissing authorization in ReadingMode
DescriptionMissing authorization in ReadingMode
ComponentReadingMode
Sub-componentUI>Accessibility>ReadingMode
Bug ClassAuthorization Error
Tracker552510359
Fix commit5b221c067c2f (chromium/src) +520/-9
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-13

Changed Functions

FunctionChangeNotes
if
chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler.cc
modified
FakeTtsEngineDelegate
chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler_browsertest.cc
modified

Files Changed

  • chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler.h
  • chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler_browsertest.cc
  • chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler.cc
From 5b221c067c2f9bc481a6b3b8203817208b9639bd Mon Sep 17 00:00:00 2001
From: Lauren Winston <[email protected]>
Date: Fri, 02 Oct 2026 12:54:48 -0700
Subject: [PATCH] [M156] Only forward a11y event data for distilled frames.

Original change's description:
> Only forward a11y event data for distilled frames.
>
> Bug: 516861674
> Change-Id: I34d0869efdaa8176e643abf1cceeb382cd053201
> Fixed: 552510359
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8468711
> Reviewed-by: Kristi Saney <[email protected]>
> Commit-Queue: Lauren Winston <[email protected]>
> Cr-Commit-Position: refs/heads/main@{#1707599}

(cherry picked from commit 33f0297a8eb5d78724669b1b352f2dea08152a3b)

Bug: 568219560,516861674,552510359
Change-Id: I34d0869efdaa8176e643abf1cceeb382cd053201
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8498523
Bot-Commit: [email protected] <[email protected]>
Auto-Submit: chrome-cherry-picker@chops-service-accounts.iam.gserviceaccount.com <chrome-cherry-picker@chops-service-accounts.iam.gserviceaccount.com>
Commit-Queue: [email protected] <[email protected]>
Cr-Commit-Position: refs/branch-heads/8078@{#233}
Cr-Branched-From: c50edc4214d81e4f944c3415d55104a49e46d214-refs/heads/main@{#1706539}
---
diff --git a/chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler.cc b/chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler.cc
index a593de7..f371744 100644
--- a/chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler.cc
+++ b/chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler.cc
@@ -34,6 +34,7 @@
 #include "chrome/common/read_anything/read_anything.mojom-shared.h"
 #include "chrome/common/read_anything/read_anything.mojom.h"
 #include "chrome/common/read_anything/read_anything_util.h"
+#include "chrome/common/webui_url_constants.h"
 #include "components/dom_distiller/content/browser/distiller_javascript_utils.h"
 #include "components/dom_distiller/content/browser/distiller_page_web_contents.h"
 #include "components/dom_distiller/core/distiller_page.h"
@@ -77,6 +78,7 @@
 #include "ui/accessibility/ax_tree_update.h"
 #include "ui/gfx/geometry/size.h"
 #include "url/gurl.h"
+#include "url/origin.h"
 #include "url/url_constants.h"
 
 #if BUILDFLAG(ENABLE_PDF)
@@ -188,6 +190,13 @@
 // Prefix definition for logging.
 constexpr char kReadAnythingPrefix[] = "Read Anything";
 
+#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
+// chrome://whats-new renders its content in a single https://www.google.com
+// child frame (see the frame-src CSP in WhatsNewUI). Reading mode distills that
+// frame directly.
+constexpr char kWhatsNewEmbeddedContentOrigin[] = "https://www.google.com";
+#endif
+
 #if BUILDFLAG(IS_CHROMEOS)
 
 InstallationState GetInstallationStateFromStatusCode(
@@ -304,6 +313,33 @@
                        : nullptr;
 }
 
+// Returns whether `rfh` is a subframe of the primary page that the primary main
+// frame can already script, i.e. it is same-origin with it. Opaque main frame
+// origins are excluded because opaque-origin frames cannot script each other.
+// file:// main frame origins are excluded because all file:// URLs share one
+// url::Origin even though Blink isolates local files from each other.
+bool IsSameOriginSubframe(content::RenderFrameHost* rfh,
+                          content::RenderFrameHost* main_frame) {
+  const url::Origin& main_origin = main_frame->GetLastCommittedOrigin();
+  return rfh->GetMainFrame() == main_frame && !main_origin.opaque() &&
+         main_origin.scheme() != url::kFileScheme &&
+         rfh->GetLastCommittedOrigin().IsSameOriginWith(main_origin);
+}
+
+// chrome://whats-new only exists on Windows, Mac and Linux.
+bool IsWhatsNewEmbeddedContentFrame(content::RenderFrameHost* rfh,
+                                    content::RenderFrameHost* main_frame) {
+#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
+  return rfh->GetParent() == main_frame &&
+         main_frame->GetLastCommittedOrigin().IsSameOriginWith(
+             GURL(chrome::kChromeUIWhatsNewURL)) &&
+         rfh->GetLastCommittedOrigin().IsSameOriginWith(
+             GURL(kWhatsNewEmbeddedContentOrigin));
+#else
+  return false;
+#endif
+}
+
 }  // namespace
 
 ReadAnythingWebContentsObserver::ReadAnythingWebContentsObserver(
@@ -576,6 +612,12 @@
 
 void ReadAnythingUntrustedPageHandler::AccessibilityEventReceived(
     const ui::AXUpdatesAndEvents& details) {
+  // Only forward accessibility data for frames that Reading Mode distills.
+  if (!IsObservingTree(details.ax_tree_id)) {
+    DVLOG(2) << "Dropping accessibility event for tree " << details.ax_tree_id;
+    return;
+  }
+  forwarded_tree_ids_.insert(details.ax_tree_id);
   page_->AccessibilityEventReceived(details.ax_tree_id, details.updates,
                                     details.events);
 }
@@ -585,7 +627,11 @@
 ///////////////////////////////////////////////////////////////////////////////
 
 void ReadAnythingUntrustedPageHandler::TreeRemoved(ui::AXTreeID ax_tree_id) {
-  page_->OnAXTreeDestroyed(ax_tree_id);
+  // The registry reports removals for every tree in the browser; only tell the
+  // renderer about trees it was sent.
+  if (forwarded_tree_ids_.erase(ax_tree_id)) {
+    page_->OnAXTreeDestroyed(ax_tree_id);
+  }
 }
 
 ///////////////////////////////////////////////////////////////////////////////
@@ -873,7 +919,10 @@
   }
 
   if (!IsPdfContents(contents, !!pdf_observer_)) {
-    return rfh == contents->GetPrimaryMainFrame();
+    // `contents` is the main observed contents here.
+    content::RenderFrameHost* main_frame = contents->GetPrimaryMainFrame();
+    return rfh == main_frame || IsSameOriginSubframe(rfh, main_frame) ||
+           IsWhatsNewEmbeddedContentFrame(rfh, main_frame);
   }
 
   content::RenderFrameHost* pdf_rfh =
@@ -1164,7 +1213,11 @@
     const GURL& image_url,
     const std::vector<SkBitmap>& bitmaps,
     const std::vector<gfx::Size>& sizes) {
-  CHECK(IsObservingTree(target_tree_id));
+  // The target frame may have been removed or navigated while the image was
+  // downloading.
+  if (!IsObservingTree(target_tree_id)) {
+    return;
+  }
   if (!ui::IsValidAXNodeIDFromRenderer(node_id)) {
     VLOG(1) << "Received image data download notification with invalid node_id "
             << node_id;
@@ -1589,7 +1642,10 @@
   // `pdf_observer_` and integrate ReadAnythingWebContentsObserver with
   // ReadAnythingUntrustedPageHandler.
   content::WebContents* pdf_contents = nullptr;
-  if (!chrome_pdf::features::IsOopifPdfEnabled()) {
+  // Only adopt the PDF guest when the tab itself is a full-page PDF. An
+  // embedded PDF on an HTML page is not distilled as a PDF.
+  if (!chrome_pdf::features::IsOopifPdfEnabled() &&
+      IsFullPagePdf(main_contents)) {
     std::vector<content::WebContents*> inner_contents =
         main_contents ? main_contents->GetInnerWebContents()
                       : std::vector<content::WebContents*>();
@@ -1629,6 +1685,7 @@
   if (pdf_rfh) {
     is_pdf_with_frame_ = true;
     VLOG(1) << "Sending pdf tree with id " << pdf_rfh->GetAXTreeID();
+    forwarded_tree_ids_.insert(pdf_rfh->GetAXTreeID());
     page_->OnActiveAXTreeIDChanged(
         pdf_rfh->GetAXTreeID(), pdf_rfh->GetPageUkmSourceId(), /*is_pdf=*/true);
   } else {
@@ -1724,6 +1781,7 @@
 
   // When IsReadAnythingWithReadabilityEnabled is true, we still send AX tree
   // for text selection.
+  forwarded_tree_ids_.insert(rfh->GetAXTreeID());
   page_->OnActiveAXTreeIDChanged(rfh->GetAXTreeID(), rfh->GetPageUkmSourceId(),
                                  /*is_pdf=*/false);
 
diff --git a/chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler.h b/chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler.h
index c012b3b5..9287297c 100644
--- a/chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler.h
+++ b/chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler.h
@@ -9,6 +9,7 @@
 #include <optional>
 #include <string>
 
+#include "base/containers/flat_set.h"
 #include "base/functional/callback_helpers.h"
 #include "base/memory/raw_ptr.h"
 #include "base/memory/safe_ref.h"
@@ -341,8 +342,14 @@
                       content::WebContents* old_contents,
                       content::WebContents* new_contents);
 
-  // Used to verify that an incoming action request is for the currently
-  // observed tree. If it's not, it may be a malicious request.
+  // Returns whether `tree_id` belongs to a frame whose accessibility data may
+  // be sent to, and whose nodes may be acted on by, the Reading Mode page.
+  // Allowed frames are:
+  //  - the primary main frame of the observed tab;
+  //  - subframes of the primary page that are same-origin with the main frame
+  //    (not when the main frame's origin is opaque or file://);
+  //  - the direct https://www.google.com child of chrome://whats-new;
+  //  - for full-page PDFs, the PDF extension host and PDF content frame.
   bool IsObservingTree(const ui::AXTreeID& tree_id) const;
 
   // Used to verify if an incoming action request (e.g. clicking a link or
@@ -506,6 +513,12 @@
   // the page would now be recognized as a pdf after it finishes loading.
   bool is_pdf_with_frame_ = false;
 
+  // IDs of trees whose data has been sent to `page_`, either through
+  // AccessibilityEventReceived or OnActiveAXTreeIDChanged. Used to forward
+  // only removals of those trees, because AXActionHandlerRegistry reports tree
+  // removals for every tree in the browser.
+  base::flat_set<ui::AXTreeID> forwarded_tree_ids_;
+
   // Subscription for tab discard events.
   base::CallbackListSubscription tab_discard_subscription_;
   base::CallbackListSubscription tab_detach_subscription_;
diff --git a/chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler_browsertest.cc b/chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler_browsertest.cc
index b4bd471..43d11503 100644
--- a/chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler_browsertest.cc
+++ b/chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler_browsertest.cc
@@ -9,14 +9,20 @@
 #include <string>
 #include <vector>
 
+#include "base/base_paths.h"
 #include "base/command_line.h"
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
 #include "base/functional/bind.h"
+#include "base/path_service.h"
 #include "base/run_loop.h"
+#include "base/strings/stringprintf.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/run_until.h"
 #include "base/test/scoped_command_line.h"
 #include "base/test/test_future.h"
 #include "base/test/values_test_util.h"
+#include "base/threading/thread_restrictions.h"
 #include "base/values.h"
 #include "chrome/browser/pdf/pdf_extension_test_util.h"
 #include "chrome/browser/profiles/profile.h"
@@ -44,6 +50,7 @@
 #include "chrome/test/user_education/mock_browser_user_education_interface.h"
 #include "components/feature_engagement/public/feature_constants.h"
 #include "components/language_detection/core/constants.h"
+#include "components/pdf/browser/pdf_frame_util.h"
 #include "components/tabs/public/tab_interface.h"
 #include "components/translate/core/browser/language_state.h"
 #include "components/translate/core/browser/translate_manager.h"
@@ -55,6 +62,8 @@
 #include "content/public/test/content_browser_test_utils.h"
 #include "content/public/test/test_web_ui.h"
 #include "extensions/browser/mime_handler/mime_handler_stream_manager.h"
+#include "net/base/filename_util.h"
+#include "net/dns/mock_host_resolver.h"
 #include "pdf/pdf_features.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "third_party/skia/include/core/SkBitmap.h"
@@ -203,6 +212,7 @@
                                          test_web_ui,
                                          /*use_screen_ai_service=*/false) {}
 #endif
+  using ReadAnythingUntrustedPageHandler::OnImageDataDownloaded;
 };
 
 class FakeTtsEngineDelegate : public content::TtsEngineDelegate {
@@ -272,6 +282,7 @@
 
   void SetUpOnMainThread() override {
     InProcessBrowserTest::SetUpOnMainThread();
+    host_resolver()->AddRule("*", "127.0.0.1");
 
     ui_test_utils::NavigateToURLWithDisposition(
         browser(), GURL(url::kAboutBlankURL),
@@ -468,6 +479,18 @@
     handler_->AccessibilityEventReceived(details);
   }
 
+  void OnImageDataDownloaded(const ui::AXTreeID& target_tree_id,
+                             ui::AXNodeID node_id,
+                             int id,
+                             int http_status_code,
+                             const GURL& image_url,
+                             const std::vector<SkBitmap>& bitmaps,
+                             const std::vector<gfx::Size>& sizes) {
+    static_cast<TestReadAnythingUntrustedPageHandler*>(handler_.get())
+        ->OnImageDataDownloaded(target_tree_id, node_id, id, http_status_code,
+                                image_url, bitmaps, sizes);
+  }
+
   void OnActiveAXTreeIDChanged() { handler_->OnActiveAXTreeIDChanged(); }
 
   void OnTranslateDriverDestroyed(translate::TranslateDriver* driver) {
@@ -1243,16 +1266,433 @@
 
 IN_PROC_BROWSER_TEST_F(ReadAnythingUntrustedPageHandlerTest,
                        AccessibilityEventReceived) {
+  handler_ = CreateHandler();
+  content::RenderFrameHost* main_frame = browser()
+                                             ->tab_strip_model()
+                                             ->GetActiveWebContents()
+                                             ->GetPrimaryMainFrame();
+
+  ui::AXUpdatesAndEvents details;
+  details.events = {};
+  details.updates = {};
+  details.ax_tree_id = main_frame->GetAXTreeID();
+
+  // The observed tab also sends its own accessibility events while the test
+  // runs; allow those so only the expectation below is checked.
+  EXPECT_CALL(page_, AccessibilityEventReceived).Times(testing::AnyNumber());
+
+  EXPECT_CALL(page_, AccessibilityEventReceived(details.ax_tree_id, _, _))
+      .Times(testing::AtLeast(1));
+  AccessibilityEventReceived(details);
+  page_.receiver_.FlushForTesting();
+}
+
+IN_PROC_BROWSER_TEST_F(ReadAnythingUntrustedPageHandlerTest,
+                       AccessibilityEventReceived_UnknownTreeNotForwarded) {
+  handler_ = CreateHandler();
+
   ui::AXUpdatesAndEvents details;
   details.events = {};
   details.updates = {};
   details.ax_tree_id = ui::AXTreeID::CreateNewAXTreeID();
-  handler_ = CreateHandler();
 
-  AccessibilityEventReceived(details);
+  // The observed tab also sends its own accessibility events while the test
+  // runs; allow those so only the expectation below is checked.
+  EXPECT_CALL(page_, AccessibilityEventReceived).Times(testing::AnyNumber());
 
   EXPECT_CALL(page_, AccessibilityEventReceived(details.ax_tree_id, _, _))
-      .Times(1);
+      .Times(0);
+  AccessibilityEventReceived(details);
+  page_.receiver_.FlushForTesting();
+}
+
+IN_PROC_BROWSER_TEST_F(ReadAnythingUntrustedPageHandlerTest,
+                       AccessibilityEventReceived_OtherTabNotForwarded) {
+  handler_ = CreateHandler();
+
+  ui_test_utils::NavigateToURLWithDisposition(
+      browser(), GURL(url::kAboutBlankURL),
+      WindowOpenDisposition::NEW_FOREGROUND_TAB,
+      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
+
+  content::RenderFrameHost* second_tab_main_frame = browser()
+                                                        ->tab_strip_model()
+                                                        ->GetActiveWebContents()
+                                                        ->GetPrimaryMainFrame();
+
+  ui::AXUpdatesAndEvents details;
+  details.events = {};
+  details.updates = {};
+  details.ax_tree_id = second_tab_main_frame->GetAXTreeID();
+
+  // The observed tab also sends its own accessibility events while the test
+  // runs; allow those so only the expectation below is checked.
+  EXPECT_CALL(page_, AccessibilityEventReceived).Times(testing::AnyNumber());
+
+  EXPECT_CALL(page_, AccessibilityEventReceived(details.ax_tree_id, _, _))
+      .Times(0);
+  AccessibilityEventReceived(details);
+  page_.receiver_.FlushForTesting();
+}
+
+IN_PROC_BROWSER_TEST_F(ReadAnythingUntrustedPageHandlerTest,
+                       AccessibilityEventReceived_CrossSiteIframeNotForwarded) {
+  base::FilePath test_data_dir;
+  ASSERT_TRUE(
+      base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &test_data_dir));
+  embedded_test_server()->ServeFilesFromDirectory(
+      test_data_dir.AppendASCII("content/test/data/"));
+  ASSERT_TRUE(embedded_test_server()->Start());
+
+  GURL url = embedded_test_server()->GetURL(
+      "a.com", "/cross_site_iframe_factory.html?a(b)");
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+
+  handler_ = CreateHandler();
+
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
+  content::RenderFrameHost* child_frame = content::ChildFrameAt(main_frame, 0);
+  ASSERT_TRUE(child_frame);
+
+  // The observed tab also sends its own accessibility events while the test
+  // runs; allow those so only the expectations below are checked.
+  EXPECT_CALL(page_, AccessibilityEventReceived).Times(testing::AnyNumber());
+
+  ui::AXUpdatesAndEvents main_details;
+  main_details.ax_tree_id = main_frame->GetAXTreeID();
+  EXPECT_CALL(page_, AccessibilityEventReceived(main_details.ax_tree_id, _, _))
+      .Times(testing::AtLeast(1));
+  AccessibilityEventReceived(main_details);
+  page_.receiver_.FlushForTesting();
... (file diff truncated)
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler_browsertest.cc b/chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler_browsertest.cc
index b4bd471..43d11503 100644
--- a/chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler_browsertest.cc
+++ b/chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler_browsertest.cc
@@ -9,14 +9,20 @@
 #include <string>
 #include <vector>
 
+#include "base/base_paths.h"
 #include "base/command_line.h"
+#include "base/files/file_util.h"
+#include "base/files/scoped_temp_dir.h"
 #include "base/functional/bind.h"
+#include "base/path_service.h"
 #include "base/run_loop.h"
+#include "base/strings/stringprintf.h"
 #include "base/test/metrics/histogram_tester.h"
 #include "base/test/run_until.h"
 #include "base/test/scoped_command_line.h"
 #include "base/test/test_future.h"
 #include "base/test/values_test_util.h"
+#include "base/threading/thread_restrictions.h"
 #include "base/values.h"
 #include "chrome/browser/pdf/pdf_extension_test_util.h"
 #include "chrome/browser/profiles/profile.h"
@@ -44,6 +50,7 @@
 #include "chrome/test/user_education/mock_browser_user_education_interface.h"
 #include "components/feature_engagement/public/feature_constants.h"
 #include "components/language_detection/core/constants.h"
+#include "components/pdf/browser/pdf_frame_util.h"
 #include "components/tabs/public/tab_interface.h"
 #include "components/translate/core/browser/language_state.h"
 #include "components/translate/core/browser/translate_manager.h"
@@ -55,6 +62,8 @@
 #include "content/public/test/content_browser_test_utils.h"
 #include "content/public/test/test_web_ui.h"
 #include "extensions/browser/mime_handler/mime_handler_stream_manager.h"
+#include "net/base/filename_util.h"
+#include "net/dns/mock_host_resolver.h"
 #include "pdf/pdf_features.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "third_party/skia/include/core/SkBitmap.h"
@@ -203,6 +212,7 @@
                                          test_web_ui,
                                          /*use_screen_ai_service=*/false) {}
 #endif
+  using ReadAnythingUntrustedPageHandler::OnImageDataDownloaded;
 };
 
 class FakeTtsEngineDelegate : public content::TtsEngineDelegate {
@@ -272,6 +282,7 @@
 
   void SetUpOnMainThread() override {
     InProcessBrowserTest::SetUpOnMainThread();
+    host_resolver()->AddRule("*", "127.0.0.1");
 
     ui_test_utils::NavigateToURLWithDisposition(
         browser(), GURL(url::kAboutBlankURL),
@@ -468,6 +479,18 @@
     handler_->AccessibilityEventReceived(details);
   }
 
+  void OnImageDataDownloaded(const ui::AXTreeID& target_tree_id,
+                             ui::AXNodeID node_id,
+                             int id,
+                             int http_status_code,
+                             const GURL& image_url,
+                             const std::vector<SkBitmap>& bitmaps,
+                             const std::vector<gfx::Size>& sizes) {
+    static_cast<TestReadAnythingUntrustedPageHandler*>(handler_.get())
+        ->OnImageDataDownloaded(target_tree_id, node_id, id, http_status_code,
+                                image_url, bitmaps, sizes);
+  }
+
   void OnActiveAXTreeIDChanged() { handler_->OnActiveAXTreeIDChanged(); }
 
   void OnTranslateDriverDestroyed(translate::TranslateDriver* driver) {
@@ -1243,16 +1266,433 @@
 
 IN_PROC_BROWSER_TEST_F(ReadAnythingUntrustedPageHandlerTest,
                        AccessibilityEventReceived) {
+  handler_ = CreateHandler();
+  content::RenderFrameHost* main_frame = browser()
+                                             ->tab_strip_model()
+                                             ->GetActiveWebContents()
+                                             ->GetPrimaryMainFrame();
+
+  ui::AXUpdatesAndEvents details;
+  details.events = {};
+  details.updates = {};
+  details.ax_tree_id = main_frame->GetAXTreeID();
+
+  // The observed tab also sends its own accessibility events while the test
+  // runs; allow those so only the expectation below is checked.
+  EXPECT_CALL(page_, AccessibilityEventReceived).Times(testing::AnyNumber());
+
+  EXPECT_CALL(page_, AccessibilityEventReceived(details.ax_tree_id, _, _))
+      .Times(testing::AtLeast(1));
+  AccessibilityEventReceived(details);
+  page_.receiver_.FlushForTesting();
+}
+
+IN_PROC_BROWSER_TEST_F(ReadAnythingUntrustedPageHandlerTest,
+                       AccessibilityEventReceived_UnknownTreeNotForwarded) {
+  handler_ = CreateHandler();
+
   ui::AXUpdatesAndEvents details;
   details.events = {};
   details.updates = {};
   details.ax_tree_id = ui::AXTreeID::CreateNewAXTreeID();
-  handler_ = CreateHandler();
 
-  AccessibilityEventReceived(details);
+  // The observed tab also sends its own accessibility events while the test
+  // runs; allow those so only the expectation below is checked.
+  EXPECT_CALL(page_, AccessibilityEventReceived).Times(testing::AnyNumber());
 
   EXPECT_CALL(page_, AccessibilityEventReceived(details.ax_tree_id, _, _))
-      .Times(1);
+      .Times(0);
+  AccessibilityEventReceived(details);
+  page_.receiver_.FlushForTesting();
+}
+
+IN_PROC_BROWSER_TEST_F(ReadAnythingUntrustedPageHandlerTest,
+                       AccessibilityEventReceived_OtherTabNotForwarded) {
+  handler_ = CreateHandler();
+
+  ui_test_utils::NavigateToURLWithDisposition(
+      browser(), GURL(url::kAboutBlankURL),
+      WindowOpenDisposition::NEW_FOREGROUND_TAB,
+      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
+
+  content::RenderFrameHost* second_tab_main_frame = browser()
+                                                        ->tab_strip_model()
+                                                        ->GetActiveWebContents()
+                                                        ->GetPrimaryMainFrame();
+
+  ui::AXUpdatesAndEvents details;
+  details.events = {};
+  details.updates = {};
+  details.ax_tree_id = second_tab_main_frame->GetAXTreeID();
+
+  // The observed tab also sends its own accessibility events while the test
+  // runs; allow those so only the expectation below is checked.
+  EXPECT_CALL(page_, AccessibilityEventReceived).Times(testing::AnyNumber());
+
+  EXPECT_CALL(page_, AccessibilityEventReceived(details.ax_tree_id, _, _))
+      .Times(0);
+  AccessibilityEventReceived(details);
+  page_.receiver_.FlushForTesting();
+}
+
+IN_PROC_BROWSER_TEST_F(ReadAnythingUntrustedPageHandlerTest,
+                       AccessibilityEventReceived_CrossSiteIframeNotForwarded) {
+  base::FilePath test_data_dir;
+  ASSERT_TRUE(
+      base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &test_data_dir));
+  embedded_test_server()->ServeFilesFromDirectory(
+      test_data_dir.AppendASCII("content/test/data/"));
+  ASSERT_TRUE(embedded_test_server()->Start());
+
+  GURL url = embedded_test_server()->GetURL(
+      "a.com", "/cross_site_iframe_factory.html?a(b)");
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+
+  handler_ = CreateHandler();
+
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
+  content::RenderFrameHost* child_frame = content::ChildFrameAt(main_frame, 0);
+  ASSERT_TRUE(child_frame);
+
+  // The observed tab also sends its own accessibility events while the test
+  // runs; allow those so only the expectations below are checked.
+  EXPECT_CALL(page_, AccessibilityEventReceived).Times(testing::AnyNumber());
+
+  ui::AXUpdatesAndEvents main_details;
+  main_details.ax_tree_id = main_frame->GetAXTreeID();
+  EXPECT_CALL(page_, AccessibilityEventReceived(main_details.ax_tree_id, _, _))
+      .Times(testing::AtLeast(1));
+  AccessibilityEventReceived(main_details);
+  page_.receiver_.FlushForTesting();
+
+  ui::AXUpdatesAndEvents child_details;
+  child_details.ax_tree_id = child_frame->GetAXTreeID();
+  EXPECT_CALL(page_, AccessibilityEventReceived(child_details.ax_tree_id, _, _))
+      .Times(0);
+  AccessibilityEventReceived(child_details);
+  page_.receiver_.FlushForTesting();
+}
+
+IN_PROC_BROWSER_TEST_F(
+    ReadAnythingUntrustedPageHandlerTest,
+    AccessibilityEventReceived_SameSiteCrossOriginIframeNotForwarded) {
+  base::FilePath test_data_dir;
+  ASSERT_TRUE(
+      base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &test_data_dir));
+  embedded_test_server()->ServeFilesFromDirectory(
+      test_data_dir.AppendASCII("content/test/data/"));
+  ASSERT_TRUE(embedded_test_server()->Start());
+
+  GURL url = embedded_test_server()->GetURL(
+      "a.com", "/cross_site_iframe_factory.html?a.com(sub.a.com)");
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+
+  handler_ = CreateHandler();
+
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
+  content::RenderFrameHost* child_frame = content::ChildFrameAt(main_frame, 0);
+  ASSERT_TRUE(child_frame);
+
+  // The observed tab also sends its own accessibility events while the test
+  // runs; allow those so only the expectation below is checked.
+  EXPECT_CALL(page_, AccessibilityEventReceived).Times(testing::AnyNumber());
+
+  ui::AXUpdatesAndEvents child_details;
+  child_details.ax_tree_id = child_frame->GetAXTreeID();
+  EXPECT_CALL(page_, AccessibilityEventReceived(child_details.ax_tree_id, _, _))
+      .Times(0);
+  AccessibilityEventReceived(child_details);
+  page_.receiver_.FlushForTesting();
+}
+
+IN_PROC_BROWSER_TEST_F(ReadAnythingUntrustedPageHandlerTest,
+                       AccessibilityEventReceived_SameOriginIframeForwarded) {
+  base::HistogramTester histogram_tester;
+  base::FilePath test_data_dir;
+  ASSERT_TRUE(
+      base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &test_data_dir));
+  embedded_test_server()->ServeFilesFromDirectory(
+      test_data_dir.AppendASCII("content/test/data/"));
+  ASSERT_TRUE(embedded_test_server()->Start());
+
+  GURL url = embedded_test_server()->GetURL(
+      "a.com", "/cross_site_iframe_factory.html?a(a)");
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+
+  SetUpHandler();
+
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
+  content::RenderFrameHost* child_frame = content::ChildFrameAt(main_frame, 0);
+  ASSERT_TRUE(child_frame);
+
+  // The observed tab also sends its own accessibility events while the test
+  // runs; allow those so only the expectation below is checked.
+  EXPECT_CALL(page_, AccessibilityEventReceived).Times(testing::AnyNumber());
+  ui::AXUpdatesAndEvents child_details;
+  child_details.ax_tree_id = child_frame->GetAXTreeID();
+  EXPECT_CALL(page_, AccessibilityEventReceived(child_details.ax_tree_id, _, _))
+      .Times(testing::AtLeast(1));
+  AccessibilityEventReceived(child_details);
+  page_.receiver_.FlushForTesting();
+
+  content::RenderFrameHost* rfh = web_contents_->GetPrimaryMainFrame();
+  GrantUserActivation(rfh);
+  handler_remote_->OnLinkClicked(child_frame->GetAXTreeID(),
+                                 /*target_node_id=*/1);
+  handler_remote_.FlushForTesting();
+  histogram_tester.ExpectUniqueSample(
+      "Accessibility.ReadAnything.RendererRequestForLinkClick.Result",
+      ReadAnythingRendererRequestResult::kAllowed,
+      /*expected_bucket_count=*/1);
+}
+
+IN_PROC_BROWSER_TEST_F(ReadAnythingUntrustedPageHandlerTest,
+                       CrossSiteIframe_ActionsRejected) {
+  base::HistogramTester histogram_tester;
+  base::FilePath test_data_dir;
+  ASSERT_TRUE(
+      base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &test_data_dir));
+  embedded_test_server()->ServeFilesFromDirectory(
+      test_data_dir.AppendASCII("content/test/data/"));
+  ASSERT_TRUE(embedded_test_server()->Start());
+
+  GURL url = embedded_test_server()->GetURL(
+      "a.com", "/cross_site_iframe_factory.html?a(b)");
+  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
+
+  SetUpHandler();
+
+  content::WebContents* web_contents =
+      browser()->tab_strip_model()->GetActiveWebContents();
+  content::RenderFrameHost* main_frame = web_contents->GetPrimaryMainFrame();
+  content::RenderFrameHost* child_frame = content::ChildFrameAt(main_frame, 0);
+  ASSERT_TRUE(child_frame);
+
+  content::RenderFrameHost* rfh = web_contents_->GetPrimaryMainFrame();
+  GrantUserActivation(rfh);
+
+  const ui::AXTreeID child_tree_id = child_frame->GetAXTreeID();
+
+  handler_remote_->OnLinkClicked(child_tree_id, /*target_node_id=*/1);
+  handler_remote_.FlushForTesting();
+  histogram_tester.ExpectUniqueSample(
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

Potential Site Isolation bypass in ReadAnything WebUI via cross-origin AXTree updates

Flapjack, 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: The ReadAnything untrusted WebUI handler registers to receive accessibility events for a tab and forwards them to the untrusted renderer process without origin filtering. If a page contains cross-origin iframes, the sensitive DOM data from these iframes is forwarded directly to the untrusted WebUI. An attacker who compromises the ReadAnything renderer can read this cross-origin data, bypassing Site Isolation.

Affected files:

  • chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler.cc
  • chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler.h

Estimated timestamp from git blame: 2023-01-11

Description

The Read Anything side panel feature operates in an untrusted WebUI renderer process (chrome-untrusted://read-anything). To function, it requires accessibility data from the active web page.

In the browser process, ReadAnythingUntrustedPageHandler instantiates a ReadAnythingWebContentsObserver to listen for accessibility events on the target WebContents. It enables accessibility via BrowserAccessibilityState::GetInstance()->CreateScopedModeForWebContents(). This instructs all frames within the tab, including cross-origin Out-of-Process Iframes (OOPIFs), to serialize their DOM state and send AXTreeUpdate objects back to the browser process.

When the browser receives these updates, WebContentsImpl::ProcessAccessibilityUpdatesAndEvents broadcasts them to all registered observers without filtering by origin. ReadAnythingWebContentsObserver::AccessibilityEventReceived catches these updates and unconditionally forwards them to ReadAnythingUntrustedPageHandler::AccessibilityEventReceived. The handler then blindly sends these updates over Mojo to the untrusted renderer:

// chrome/browser/ui/webui/side_panel/read_anything/read_anything_untrusted_page_handler.cc
void ReadAnythingUntrustedPageHandler::AccessibilityEventReceived(
    const ui::AXUpdatesAndEvents& details) {
  page_->AccessibilityEventReceived(details.ax_tree_id, details.updates,
                                    details.events);
}

This behavior violates Chromium’s Site Isolation guidelines (docs/security/ax-tree-security-guidelines.md), which state that the accessibility tree “holds sensitive content that should not be passed back down to renderers to avoid data leaks between them.”

Potential Attacker Steps

(Note: These are suggested steps for exploitation; our tooling agent does not yet have the ability to run code to verify an end-to-end exploit chain.)

  1. An attacker hosts a malicious website (e.g., attacker.com) and embeds a cross-origin iframe targeting sensitive user data (e.g., bank.com).
  2. The user visits the attacker’s site and opens the Read Anything side panel (or the attacker induces the user to open it).
  3. The browser enables accessibility mode, prompting the isolated bank.com renderer to serialize its DOM and send accessibility updates to the browser process.
  4. The browser process forwards these unfiltered cross-origin updates directly to the chrome-untrusted://read-anything renderer process.
  5. The attacker exploits a memory corruption or logic vulnerability in the Read Anything WebUI renderer (e.g., by sending malformed AXTree data from the attacker.com main frame) to gain code execution within the WebUI process.
  6. The compromised WebUI renderer extracts the leaked accessibility tree updates for bank.com from its memory, successfully stealing cross-origin sensitive data and bypassing Site Isolation.

Suggested Fix

Accessibility updates should be filtered before they are forwarded to the untrusted renderer process. In ReadAnythingUntrustedPageHandler::AccessibilityEventReceived, verify that the ax_tree_id in the AXUpdatesAndEvents struct belongs to a frame that the WebUI is explicitly permitted to observe (typically the primary main frame).

Similar to how ReadAnythingUntrustedPageHandler::IsObservingTree currently restricts incoming WebUI actions to the primary main frame, a strict frame-matching check should be applied to outgoing AXTreeUpdate messages to drop cross-origin iframe data.

Evaluated with Chrome root at commit: ae14e9176a277261f3c963589b6b045c43cec1dc


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