Chrome · ReadingMode › Reading mode
CVE-2026-113274
UAF in ReadingMode (Reading mode)
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
forchrome/browser/resources/side_panel/read_anything/content/content_controller.ts |
modified | |
ifchrome/browser/resources/side_panel/read_anything/content/content_controller.ts |
modified | |
whilechrome/browser/resources/side_panel/read_anything/content/content_controller.ts |
modified |
Files Changed
chrome/test/data/webui/side_panel/read_anything/content_controller_test.tschrome/browser/resources/side_panel/read_anything/app/app.csschrome/browser/resources/side_panel/read_anything/content/content_controller.ts
Patch
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.
References
On This Page