Low chrome UAF 🔧 Commit mapped

Overview

Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in ReadingMode
DescriptionUse after free in ReadingMode
ComponentReadingMode
Sub-componentReading mode
Bug ClassUAF
Tracker570715681
Fix commite6b44e68aad1 (chromium/src) +63/-22
CISA KEVNot listed
Creditedchipsec
Disclosed2026-10-20

Changed Functions

FunctionChangeNotes
for
chrome/browser/resources/side_panel/read_anything/content/content_controller.ts
modified
if
chrome/browser/resources/side_panel/read_anything/content/content_controller.ts
modified
while
chrome/browser/resources/side_panel/read_anything/content/content_controller.ts
modified

Files Changed

  • chrome/test/data/webui/side_panel/read_anything/content_controller_test.ts
  • chrome/browser/resources/side_panel/read_anything/app/app.css
  • chrome/browser/resources/side_panel/read_anything/content/content_controller.ts
From e6b44e68aad14dcfc0ec19eb5c082ec18e42516b Mon Sep 17 00:00:00 2001
From: Lauren Winston <lwinston@google.com>
Date: Fri, 09 Oct 2026 14:06:27 -0700
Subject: [PATCH] [Reading mode] Expand setHTML options for distilled content.

Fixed:570715681

Change-Id: I278e696cf5f719c0f45b147533de6f15fbc88f15
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8549354
Commit-Queue: Lauren Winston <lwinston@google.com>
Reviewed-by: Kristi Saney <kristislee@google.com>
Cr-Commit-Position: refs/heads/main@{#1715994}
---
diff --git a/chrome/browser/resources/side_panel/read_anything/app/app.css b/chrome/browser/resources/side_panel/read_anything/app/app.css
index 42ddd578..34234834 100644
--- a/chrome/browser/resources/side_panel/read_anything/app/app.css
+++ b/chrome/browser/resources/side_panel/read_anything/app/app.css
@@ -20,7 +20,7 @@
   --immersive-menu-font-weight: 400;
 }
 
-#lineFocus {
+:host > #appFlexParent > #lineFocus {
   position: absolute;
   top: var(--line-focus-y);
   left: var(--line-focus-left);
@@ -35,17 +35,17 @@
   z-index: 9997;
 }
 
-#lineFocus.line-mode {
+:host > #appFlexParent > #lineFocus.line-mode {
   --line-focus-left: var(--sp-card-block-padding);
   --line-focus-right: var(--sp-card-block-padding);
 }
 
-#lineFocus.window-mode {
+:host > #appFlexParent > #lineFocus.window-mode {
   --line-focus-left: 0;
   --line-focus-right: 0;
 }
 
-.immersive.full-page #lineFocus.line-mode {
+:host > #appFlexParent.immersive.full-page > #lineFocus.line-mode {
   --line-focus-left: var(--sp-card-block-padding);
   --line-focus-right: var(--immersive-scrollbar-width);
 }
@@ -238,7 +238,7 @@
     color: HighlightText;
   }
 
-  #lineFocus.line-mode {
+  :host > #appFlexParent > #lineFocus.line-mode {
     background-color: Highlight;
   }
 
@@ -253,7 +253,7 @@
    * without this rule, the line focus scrim will not work and therefore might
    * not support the user's color and contrast needs, this is an acceptable
    * use of forced-color-adjust: none */
-  #lineFocus.window-mode {
+  :host > #appFlexParent > #lineFocus.window-mode {
     forced-color-adjust: none;
   }
 
@@ -366,7 +366,7 @@
   clip-path: inset(0);
 }
 
-.settings-overlay {
+:host > #appFlexParent > .settings-overlay {
   width: 100vw;
   height: 100vh;
   position: fixed;
diff --git a/chrome/browser/resources/side_panel/read_anything/content/content_controller.ts b/chrome/browser/resources/side_panel/read_anything/content/content_controller.ts
index 815cdfa0..137db67 100644
--- a/chrome/browser/resources/side_panel/read_anything/content/content_controller.ts
+++ b/chrome/browser/resources/side_panel/read_anything/content/content_controller.ts
@@ -23,9 +23,19 @@
 import {removeExtraneousElementsFrom} from './readability_content_processing.js';
 import {ReadabilityImageClassifier} from './readability_image_classifier.js';
 
+// Sanitizer options for distilled HTML rendered in Reading mode.
 const READABILITY_SET_HTML_OPTIONS: SetHtmlOptions = {
   sanitizer: {
-    removeElements: ['style', 'meta', 'template'],
+    removeElements: [
+      'dialog',
+      'link',
+      'meta',
+      'style',
+      'template',
+      // Also remove <style> elements inside <svg>, which use the SVG namespace.
+      {name: 'style', namespace: 'http://www.w3.org/2000/svg'},
+    ],
+    removeAttributes: ['class', 'data-link', 'popover', 'style'],
   },
 };
 
@@ -76,6 +86,9 @@
   ['em', 'b'],
 ]);
 
+const READABILITY_REPLACED_ELEMENTS_SELECTOR =
+    [...READABILITY_TAG_TO_RM_TAG.keys(), ':not(:defined)'].join(',');
+
 // The tags of the elements that are registered as blocks in the NodeStore when
 // block tracking is enabled (see ContentController.buildSubtree_). A block
 // is an element whose text is translated as a unit, and which translation
@@ -328,11 +341,12 @@
       }
 
       // Replace tags that shouldn't be interactive or have special behavior
-      // in reading mode. This is similar to what happens in `buildSubtree_`
-      // for Screen2x.
-      for (const [tag, replacement] of READABILITY_TAG_TO_RM_TAG) {
-        const elements = contentContainer.querySelectorAll(tag);
-        for (const element of elements) {
+      // in reading mode, and unwrap any custom elements defined in the Reading
+      // Mode WebUI before connecting to the shadow root.
+      for (const element of contentContainer.querySelectorAll(
+               READABILITY_REPLACED_ELEMENTS_SELECTOR)) {
+        const replacement = READABILITY_TAG_TO_RM_TAG.get(element.localName);
+        if (replacement) {
           const replacementEl = document.createElement(replacement);
           while (element.firstChild) {
             replacementEl.appendChild(element.firstChild);
@@ -341,6 +355,8 @@
             replacementEl.setAttribute(attr.name, attr.value);
           }
           element.replaceWith(replacementEl);
+        } else if (customElements.get(element.localName)) {
+          element.replaceWith(...element.childNodes);
         }
       }
 
diff --git a/chrome/test/data/webui/side_panel/read_anything/content_controller_test.ts b/chrome/test/data/webui/side_panel/read_anything/content_controller_test.ts
index 474e76ad..ba5214d 100644
--- a/chrome/test/data/webui/side_panel/read_anything/content_controller_test.ts
+++ b/chrome/test/data/webui/side_panel/read_anything/content_controller_test.ts
@@ -2397,24 +2397,49 @@
           assertStringContains(root.textContent || '', 'Normal text');
         });
 
-    test('strips style, meta, and template elements', async () => {
+    test('strips disallowed elements and attributes', async () => {
+      if (!customElements.get('read-anything-toolbar')) {
+        customElements.define(
+            'read-anything-toolbar', class extends HTMLElement {});
+      }
       contentBrowserProxy.htmlContent =
+          '<div class=" immersive " style="position:fixed" ' +
+          'popover="manual" data-link="https://example.com">Safe text' +
           '<style>body { display: none; }</style>' +
+          '<svg><style>* { background: red; }</style></svg>' +
+          '<link rel="stylesheet" href="chrome://resources/css/text_defaults.css">' +
+          '<dialog open>Dialog</dialog>' +
           '<meta http-equiv="refresh" content="0;url=https://example.com">' +
           '<template><p>Hidden</p></template>' +
-          '<p>Safe text <a href="javascript:void(0)">Link</a></p>';
+          '<read-anything-toolbar><span>Inside toolbar</span></read-anything-toolbar>' +
+          '<mjx-container><span>Math content</span></mjx-container>' +
+          '<a href="javascript:void(0)">Link</a>' +
+          '</div>';
 
-      const root = contentController.updateContent() as DocumentFragment;
+      const content = contentController.updateContent() as DocumentFragment;
       await microtasksFinished();
 
-      assertTrue(!!root);
-      assertEquals(null, root.querySelector('style'));
-      assertEquals(null, root.querySelector('meta'));
-      assertEquals(null, root.querySelector('template'));
-      const link = root.querySelector('a');
+      assertTrue(!!content);
+      assertEquals(null, content.querySelector('style'));
+      assertEquals(null, content.querySelector('link'));
+      assertEquals(null, content.querySelector('dialog'));
+      assertEquals(null, content.querySelector('meta'));
+      assertEquals(null, content.querySelector('template'));
+      assertEquals(null, content.querySelector('read-anything-toolbar'));
+      assertTrue(!!content.querySelector('mjx-container'));
+
+      const div = content.querySelector('div > div');
+      assertTrue(!!div);
+      assertFalse(div.hasAttribute('class'));
+      assertFalse(div.hasAttribute('style'));
+      assertFalse(div.hasAttribute('popover'));
+      assertFalse(div.hasAttribute('data-link'));
+      const link = content.querySelector('a');
       assertTrue(!!link);
       assertEquals('', link.getAttribute('href'));
-      assertStringContains(root.textContent || '', 'Safe text');
+      assertStringContains(content.textContent || '', 'Safe text');
+      assertStringContains(content.textContent || '', 'Inside toolbar');
+      assertStringContains(content.textContent || '', 'Math content');
     });
   });
 
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/test/data/webui/side_panel/read_anything/content_controller_test.ts b/chrome/test/data/webui/side_panel/read_anything/content_controller_test.ts
index 474e76ad..ba5214d 100644
--- a/chrome/test/data/webui/side_panel/read_anything/content_controller_test.ts
+++ b/chrome/test/data/webui/side_panel/read_anything/content_controller_test.ts
@@ -2397,24 +2397,49 @@
           assertStringContains(root.textContent || '', 'Normal text');
         });
 
-    test('strips style, meta, and template elements', async () => {
+    test('strips disallowed elements and attributes', async () => {
+      if (!customElements.get('read-anything-toolbar')) {
+        customElements.define(
+            'read-anything-toolbar', class extends HTMLElement {});
+      }
       contentBrowserProxy.htmlContent =
+          '<div class=" immersive " style="position:fixed" ' +
+          'popover="manual" data-link="https://example.com">Safe text' +
           '<style>body { display: none; }</style>' +
+          '<svg><style>* { background: red; }</style></svg>' +
+          '<link rel="stylesheet" href="chrome://resources/css/text_defaults.css">' +
+          '<dialog open>Dialog</dialog>' +
           '<meta http-equiv="refresh" content="0;url=https://example.com">' +
           '<template><p>Hidden</p></template>' +
-          '<p>Safe text <a href="javascript:void(0)">Link</a></p>';
+          '<read-anything-toolbar><span>Inside toolbar</span></read-anything-toolbar>' +
+          '<mjx-container><span>Math content</span></mjx-container>' +
+          '<a href="javascript:void(0)">Link</a>' +
+          '</div>';
 
-      const root = contentController.updateContent() as DocumentFragment;
+      const content = contentController.updateContent() as DocumentFragment;
       await microtasksFinished();
 
-      assertTrue(!!root);
-      assertEquals(null, root.querySelector('style'));
-      assertEquals(null, root.querySelector('meta'));
-      assertEquals(null, root.querySelector('template'));
-      const link = root.querySelector('a');
+      assertTrue(!!content);
+      assertEquals(null, content.querySelector('style'));
+      assertEquals(null, content.querySelector('link'));
+      assertEquals(null, content.querySelector('dialog'));
+      assertEquals(null, content.querySelector('meta'));
+      assertEquals(null, content.querySelector('template'));
+      assertEquals(null, content.querySelector('read-anything-toolbar'));
+      assertTrue(!!content.querySelector('mjx-container'));
+
+      const div = content.querySelector('div > div');
+      assertTrue(!!div);
+      assertFalse(div.hasAttribute('class'));
+      assertFalse(div.hasAttribute('style'));
+      assertFalse(div.hasAttribute('popover'));
+      assertFalse(div.hasAttribute('data-link'));
+      const link = content.querySelector('a');
       assertTrue(!!link);
       assertEquals('', link.getAttribute('href'));
-      assertStringContains(root.textContent || '', 'Safe text');
+      assertStringContains(content.textContent || '', 'Safe text');
+      assertStringContains(content.textContent || '', 'Inside toolbar');
+      assertStringContains(content.textContent || '', 'Math content');
     });
   });
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.