CVE-2026-8539
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fthird_party/blink/renderer/core/dom/document_test.cc |
modified |
Files Changed
third_party/blink/renderer/core/dom/document.ccthird_party/blink/renderer/core/dom/document_test.ccthird_party/blink/renderer/core/html/parser/fragment_parser.ccthird_party/blink/renderer/core/sanitizer/sanitizer_api.ccthird_party/blink/web_tests/external/wpt/sanitizer-api/sethtml-tree-construction.tentative.html
Patch
From 54315b858250350774108ce6b986606e0d77efe7 Mon Sep 17 00:00:00 2001
From: Daniel Vogelheim <vogelheim@chromium.org>
Date: Thu, 02 Apr 2026 05:39:20 -0700
Subject: [PATCH] [Sanitizer] Ensure content is removed when an exception is thrown.
When the sanitize operation throws an exception, we don't clear the
parse result. Also, the callers will pass through the exception, but
will return the incomplete result to the callers.
Fixed: 496524586
Change-Id: I3a6402afacb5d5275f546dfdefdac5d270e96d1f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7705032
Reviewed-by: Noam Rosenthal <nrosenthal@google.com>
Reviewed-by: Joey Arhar <jarhar@chromium.org>
Commit-Queue: Daniel Vogelheim <vogelheim@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1609133}
---
diff --git a/third_party/blink/renderer/core/dom/document.cc b/third_party/blink/renderer/core/dom/document.cc
index 75dd7e4..9fea619a 100644
--- a/third_party/blink/renderer/core/dom/document.cc
+++ b/third_party/blink/renderer/core/dom/document.cc
@@ -10178,6 +10178,9 @@
/*context_element*/ doc, /*root_element*/ doc,
FragmentParserOptions(options),
exception_state);
+ if (exception_state.HadException()) {
+ return nullptr;
+ }
return doc;
}
@@ -10192,6 +10195,9 @@
/*context_element*/ doc, /*root_element*/ doc,
FragmentParserOptions(options),
exception_state);
+ if (exception_state.HadException()) {
+ return nullptr;
+ }
return doc;
}
diff --git a/third_party/blink/renderer/core/dom/document_test.cc b/third_party/blink/renderer/core/dom/document_test.cc
index 4c3a20a..d91c3a5 100644
--- a/third_party/blink/renderer/core/dom/document_test.cc
+++ b/third_party/blink/renderer/core/dom/document_test.cc
@@ -50,7 +50,14 @@
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_dom_exception.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_sanitizer_config.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_set_html_options.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_set_html_unsafe_options.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_throw_dom_exception.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_union_sanitizer_sanitizerconfig_sanitizerpresets.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_union_sanitizerelementnamespace_string.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_union_sanitizerelementnamespacewithattributes_string.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_union_string_trustedhtml.h"
#include "third_party/blink/renderer/core/css/media_query_list_listener.h"
#include "third_party/blink/renderer/core/css/media_query_matcher.h"
#include "third_party/blink/renderer/core/css/style_engine.h"
@@ -2253,4 +2260,43 @@
}
#endif // BUILDFLAG(IS_ANDROID)
+TEST_F(DocumentTest, ParseHTMLSanitizerException) {
+ // This is a regression test for https://crbug.com/496524586.
+
+ // SanitizerConfig equivalent to: {elements: ["div"], removeElements: ["div"]}
+ SanitizerConfig* config = SanitizerConfig::Create();
+ config->setElements({MakeGarbageCollected<
+ V8UnionSanitizerElementNamespaceWithAttributesOrString>("div")});
+ config->setRemoveElements(
+ {MakeGarbageCollected<V8UnionSanitizerElementNamespaceOrString>("div")});
+
+ {
+ DummyExceptionStateForTesting exception_state;
+ SetHTMLOptions* options = MakeGarbageCollected<SetHTMLOptions>();
+ options->setSanitizer(
+ MakeGarbageCollected<
+ V8UnionSanitizerOrSanitizerConfigOrSanitizerPresets>(config));
+ Document* doc =
+ Document::parseHTML(GetDocument().GetExecutionContext(), "test string",
+ options, exception_state);
+ EXPECT_EQ(doc, nullptr);
+ EXPECT_TRUE(exception_state.HadException());
+ }
+
+ {
+ DummyExceptionStateForTesting exception_state;
+ SetHTMLUnsafeOptions* options =
+ MakeGarbageCollected<SetHTMLUnsafeOptions>();
+ options->setSanitizer(
+ MakeGarbageCollected<
+ V8UnionSanitizerOrSanitizerConfigOrSanitizerPresets>(config));
+ Document* doc = Document::parseHTMLUnsafe(
+ GetDocument().GetExecutionContext(),
+ MakeGarbageCollected<V8UnionStringOrTrustedHTML>("test string"),
+ options, exception_state);
+ EXPECT_EQ(doc, nullptr);
+ EXPECT_TRUE(exception_state.HadException());
+ }
+}
+
} // namespace blink
diff --git a/third_party/blink/renderer/core/html/parser/fragment_parser.cc b/third_party/blink/renderer/core/html/parser/fragment_parser.cc
index 69c78b4c..4aef43b8 100644
--- a/third_party/blink/renderer/core/html/parser/fragment_parser.cc
+++ b/third_party/blink/renderer/core/html/parser/fragment_parser.cc
@@ -253,6 +253,9 @@
exception_state);
}
+ if (exception_state.HadException()) {
+ return nullptr;
+ }
return fragment;
}
diff --git a/third_party/blink/renderer/core/sanitizer/sanitizer_api.cc b/third_party/blink/renderer/core/sanitizer/sanitizer_api.cc
index afa2c4e..cc815ed 100644
--- a/third_party/blink/renderer/core/sanitizer/sanitizer_api.cc
+++ b/third_party/blink/renderer/core/sanitizer/sanitizer_api.cc
@@ -68,8 +68,8 @@
const Sanitizer* sanitizer =
SanitizerFromOptions(options, mode, exception_state);
-
if (exception_state.HadException()) {
+ root_element->setTextContent("");
return;
}
diff --git a/third_party/blink/web_tests/external/wpt/sanitizer-api/sethtml-tree-construction.tentative.html b/third_party/blink/web_tests/external/wpt/sanitizer-api/sethtml-tree-construction.tentative.html
index 359560d..985b18eb 100644
--- a/third_party/blink/web_tests/external/wpt/sanitizer-api/sethtml-tree-construction.tentative.html
+++ b/third_party/blink/web_tests/external/wpt/sanitizer-api/sethtml-tree-construction.tentative.html
@@ -24,6 +24,7 @@
assert_equals(
error.name, testcase.error,
`Expect exception ${testcase.error}, but got ${error}.`);
+ assert_equals(div.firstChild, null);
return; // Early return in order to not trigger the subsequent
// assertions.
}
Regression Test / PoC
diff --git a/third_party/blink/renderer/core/dom/document_test.cc b/third_party/blink/renderer/core/dom/document_test.cc
index 4c3a20a..d91c3a5 100644
--- a/third_party/blink/renderer/core/dom/document_test.cc
+++ b/third_party/blink/renderer/core/dom/document_test.cc
@@ -50,7 +50,14 @@
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_dom_exception.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_sanitizer_config.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_set_html_options.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_set_html_unsafe_options.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_throw_dom_exception.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_union_sanitizer_sanitizerconfig_sanitizerpresets.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_union_sanitizerelementnamespace_string.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_union_sanitizerelementnamespacewithattributes_string.h"
+#include "third_party/blink/renderer/bindings/core/v8/v8_union_string_trustedhtml.h"
#include "third_party/blink/renderer/core/css/media_query_list_listener.h"
#include "third_party/blink/renderer/core/css/media_query_matcher.h"
#include "third_party/blink/renderer/core/css/style_engine.h"
@@ -2253,4 +2260,43 @@
}
#endif // BUILDFLAG(IS_ANDROID)
+TEST_F(DocumentTest, ParseHTMLSanitizerException) {
+ // This is a regression test for https://crbug.com/496524586.
+
+ // SanitizerConfig equivalent to: {elements: ["div"], removeElements: ["div"]}
+ SanitizerConfig* config = SanitizerConfig::Create();
+ config->setElements({MakeGarbageCollected<
+ V8UnionSanitizerElementNamespaceWithAttributesOrString>("div")});
+ config->setRemoveElements(
+ {MakeGarbageCollected<V8UnionSanitizerElementNamespaceOrString>("div")});
+
+ {
+ DummyExceptionStateForTesting exception_state;
+ SetHTMLOptions* options = MakeGarbageCollected<SetHTMLOptions>();
+ options->setSanitizer(
+ MakeGarbageCollected<
+ V8UnionSanitizerOrSanitizerConfigOrSanitizerPresets>(config));
+ Document* doc =
+ Document::parseHTML(GetDocument().GetExecutionContext(), "test string",
+ options, exception_state);
+ EXPECT_EQ(doc, nullptr);
+ EXPECT_TRUE(exception_state.HadException());
+ }
+
+ {
+ DummyExceptionStateForTesting exception_state;
+ SetHTMLUnsafeOptions* options =
+ MakeGarbageCollected<SetHTMLUnsafeOptions>();
+ options->setSanitizer(
+ MakeGarbageCollected<
+ V8UnionSanitizerOrSanitizerConfigOrSanitizerPresets>(config));
+ Document* doc = Document::parseHTMLUnsafe(
+ GetDocument().GetExecutionContext(),
+ MakeGarbageCollected<V8UnionStringOrTrustedHTML>("test string"),
+ options, exception_state);
+ EXPECT_EQ(doc, nullptr);
+ EXPECT_TRUE(exception_state.HadException());
+ }
+}
+
} // namespace blink
diff --git a/third_party/blink/web_tests/external/wpt/sanitizer-api/sethtml-tree-construction.tentative.html b/third_party/blink/web_tests/external/wpt/sanitizer-api/sethtml-tree-construction.tentative.html
index 359560d..985b18eb 100644
--- a/third_party/blink/web_tests/external/wpt/sanitizer-api/sethtml-tree-construction.tentative.html
+++ b/third_party/blink/web_tests/external/wpt/sanitizer-api/sethtml-tree-construction.tentative.html
@@ -24,6 +24,7 @@
assert_equals(
error.name, testcase.error,
`Expect exception ${testcase.error}, but got ${error}.`);
+ assert_equals(div.firstChild, null);
return; // Early return in order to not trigger the subsequent
// assertions.
}
Original Bug Report
setHTML() fails open on invalid SanitizerConfig, inserting unsanitized HTML with active scripts into the live DOM
Report description
setHTML() fails open on invalid SanitizerConfig, inserting unsanitized HTML with active scripts into the live DOM
Bug location
Where do you want to report your vulnerability?
Chrome VRP – Report security issues affecting the Chrome browser. See program rules
Which URL (or repository) have you found the vulnerability in?
https://chromium.googlesource.com/chromium/src
The problem
Please describe the technical details of the vulnerability
Summary
Element.setHTML() fails open on invalid SanitizerConfig, inserting unsanitized HTML with active scripts into the live DOM.
When Sanitizer::Create throws a TypeError due to an invalid config (e.g., both elements and removeElements specified), the parsed but unsanitized DocumentFragment is inserted into the DOM. Event handler attributes such as ontoggle and onerror execute JavaScript immediately after insertion.
Chrome Version
- Tested on Chrome 146.0.7680.164 (Official Build, 64-bit), Linux x86_64
- Affected on All Chrome versions with the Sanitizer API enabled
Root Cause
SanitizeSafeInternal does not clear fragment on Sanitizer creation failure
File: third_party/blink/renderer/core/sanitizer/sanitizer_api.cc
When SanitizerFromSafeOptions fails, the function returns without clearing the fragment at lines 107–109:
if (exception_state.HadException()) {
return; // BUG: does NOT clear root_element
}
The earlier error path in the same function at lines 90–92 correctly clears it:
if (exception_state.HadException()) {
root_element->setTextContent(""); // CORRECT: clears unsanitized content
return;
}
The unsanitized fragment is unconditionally inserted into the live DOM. The TypeError propagates to JS, but the DOM is already modified and event handlers have already fired.
Reproduction
Sanitizer::setFrom() returns false when the config is invalid per spec:
- Both
elementsandremoveElementspresent (sanitizer.cc:1170) - Both
attributesandremoveAttributespresent (sanitizer.cc:1175) elementsandreplaceWithChildrenElementsoverlap (sanitizer.cc:1184)- Duplicate entries in any list (sanitizer.cc:1058)
Sanitizer::Create then throws TypeError at sanitizer.cc:65, entering the fail-open path.
Example
var el = document.createElement('div');
document.body.appendChild(el);
try {
el.setHTML(
'<details open ontoggle="alert(document.domain)"><summary>x</summary></details>' +
'<img src=x onerror="alert(\'onerror XSS\')">',
{ sanitizer: { elements: ['div'], removeElements: ['span'] } }
);
} catch(e) { console.log(e.message); }
console.log(el.innerHTML);
Expected: TypeError thrown, element remains empty and no script execution.
Actual: TypeError thrown but element contains unsanitized HTML. alert(document.domain) fires via ontoggle. alert('onerror XSS') fires via onerror. <script>, <iframe>, javascript: URLs all survive in the DOM.
Suggested Fix
Clear fragment on exception in SanitizeSafeInternal
The bug is that the error path at lines 107–109 does not clear the fragment, unlike the correct pattern at lines 90–92. The fix is to add root_element->setTextContent("") so all error paths consistently clear unsanitized content.
File: third_party/blink/renderer/core/sanitizer/sanitizer_api.cc
SanitizeSafeInternal, around line 107:
const Sanitizer* sanitizer =
SanitizerFromSafeOptions(options, exception_state);
if (exception_state.HadException()) {
+ root_element->setTextContent("");
return;
}
This matches the existing correct pattern already used earlier:
// Lines 90-92 (SanitizeSafeInternal)
if (exception_state.HadException()) {
root_element->setTextContent(""); // Already correctly clears fragment here
return;
}
Impact analysis
setHTML() fails to sanitize HTML when given an invalid SanitizerConfig. The unsanitized content, including event handlers and script tags, is inserted into the live DOM and executes. This violates the spec guarantee that setHTML() always strips XSS-unsafe content.
The cause
What version of Chrome have you found the security issue in?
146.0.7680.164 stable
Is the security issue related to a crash?
No, it is not related to a crash.
Choose the type of vulnerability
Cross-site scripting (XSS)
How would you like to be publicly acknowledged for your report?
Jungwoo Lee (@physicube) and Wongi Lee (@_qwerty_po)