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 <[email protected]> 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 <[email protected]> Reviewed-by: Kristi Saney <[email protected]> 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