CVE-2025-8582
Overview
Background
- Charset auto-detection
- Blink’s fallback that sniffs a document’s character encoding from its bytes when no encoding is declared via HTTP header or
<meta>tag. - ISO-2022-JP
- a stateful Japanese encoding that uses in-band escape sequences (e.g.
ESC $ B/ESC ( B) to switch modes, so the same bytes can be reinterpreted as different characters depending on decoder state. - `TextResourceDecoder`
- the Blink component that decodes resource bytes to text and, when configured with auto-detection, reports a detected charset back to the parser via
auto_detected_charset. - `RuntimeEnabledFeatures::RemoveCharsetAutoDetectionForISO2022JPEnabled()`
- a new stable-by-default kill-switch flag that, when enabled, suppresses auto-detecting ISO-2022-JP for HTML/XML.
Root Cause Analysis
When an HTML/XML document arrived without a declared charset, TextResourceDecoder ran encoding detection and, if the detector returned ISO-2022-JP, adopted it via SetEncoding(...) and reported it through auto_detected_charset. Because ISO-2022-JP is escape-driven, bytes that appear inert as ASCII/windows-1252 can be reinterpreted once the decoder enters a different mode, so allowing untrusted content to auto-select this encoding lets attacker bytes mutate into active markup — the removed inspector test resource iso_2022_jp.html is literally titled XSS and embeds ISO-2022-JP escape bytes inside image alt attributes. The invariant violated is that encoding selection for undeclared HTML must not let content silently switch to an encoding that changes the meaning of already-parsed bytes.
The fix, gated by RemoveCharsetAutoDetectionForISO2022JPEnabled(), adds an early return in the auto-detection routine when the detected charset is ISO-2022-JP for kHTMLContent/kXMLContent, so neither *auto_detected_charset nor SetEncoding() is applied and the document falls back to a non-stateful default (windows-1252 in the test expectations). Note the statement reordering: SetEncoding(detected_encoding, ...) was moved to after the ISO-2022-JP guard so the early return prevents the encoding from being set at all. This is a tightening of what auto-detection is permitted to select.
Attack Path
- Serve undeclared HTML
An attacker serves an HTML response with no charset in the HTTP
Content-Typeheader and no<meta charset>, forcing Blink into auto-detection. - Craft ISO-2022-JP escape bytes
The body embeds ISO-2022-JP escape sequences (e.g.
0x1b 0x24 0x42 ... 0x1b 0x28 0x42) positioned so mode switches change how surrounding bytes decode, as in the removediso_2022_jp.htmlresource with attribute-embedded escapes. - Detector selects ISO-2022-JP
Pre-patch,
DetectTextEncodingreturns ISO-2022-JP, the decoder callsSetEncoding()and reports it viaauto_detected_charset, decoding the page under the stateful encoding. - Byte reinterpretation yields injected markup
Content the author/site treated as inert text (e.g. quoted attribute data) is re-parsed as new tags/attributes, enabling script injection / XSS
(inferred as the concrete sink); the patch establishes only that ISO-2022-JP auto-selection was possible and is now blocked.
Impact Assessment
(inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/dom/decoded_data_document_parser.cc |
modified | |
ifthird_party/blink/renderer/core/html/parser/text_resource_decoder.cc |
modified | |
TESTthird_party/blink/renderer/core/html/parser/text_resource_decoder_test.cc |
modified |
Files Changed
chrome/browser/chrome_web_platform_security_metrics_browsertest.ccthird_party/blink/renderer/core/dom/decoded_data_document_parser.ccthird_party/blink/renderer/core/frame/deprecation/deprecation.json5third_party/blink/renderer/core/html/parser/text_resource_decoder.ccthird_party/blink/renderer/core/html/parser/text_resource_decoder_test.ccthird_party/blink/renderer/platform/runtime_enabled_features.json5third_party/blink/web_tests/external/wpt/encoding-detection/ja-ISO-2022-JP-late.tentative-expected.txt
Audit Directions
- Stateful/escape-driven encodings in auto-detectionReview which encodings the sniffer may auto-select for HTML/XML and flag any (like ISO-2022-JP) whose in-band state can reinterpret earlier bytes.
- Encoding selection orderingVerify that any
SetEncoding()/charset-adoption call is reached only after all suppression guards, so an early-return path cannot leave a dangerous encoding partially applied. - Undeclared-charset fallback pathsAudit content types that fall into detection when no charset is declared, ensuring the fallback cannot change the security-relevant parse of untrusted bytes.
Patch
From a2d76e87da6b467dfcc11e84327e7c00f0a1b5a7 Mon Sep 17 00:00:00 2001
From: Jun Kokatsu <jkokatsu@google.com>
Date: Wed, 18 Jun 2025 09:06:21 -0700
Subject: [PATCH] Disable auto-detection of ISO-2022-JP charset
Per approval in the intent to ship thread[1], this CL removes support
for auto-detection of ISO-2022-JP charset in HTML.
Timelines:
- Warning messages added in [2], in M137.
- Flag added and disabled, now, in M139.
- Flag and code can be removed in M141 or so, once there are no problems
when M139 goes to stable.
[1]:
https://groups.google.com/a/chromium.org/g/blink-dev/c/yIgrr5YNGJ4/m/jKvlEerFCAAJ
[2]:
https://chromiumdash.appspot.com/commit/0dd1c5ff57ca550269c61455b9a3385289c033bc
Bug: 40089450
Change-Id: Id3bd642da8c630188775276909e6e2bffbad174c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6378605
Auto-Submit: Jun Kokatsu <jkokatsu@google.com>
Reviewed-by: Mason Freed <masonf@chromium.org>
Reviewed-by: Charlie Reis <creis@chromium.org>
Commit-Queue: Mason Freed <masonf@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1475633}
---
diff --git a/chrome/browser/chrome_web_platform_security_metrics_browsertest.cc b/chrome/browser/chrome_web_platform_security_metrics_browsertest.cc
index 531f43f..81bb210 100644
--- a/chrome/browser/chrome_web_platform_security_metrics_browsertest.cc
+++ b/chrome/browser/chrome_web_platform_security_metrics_browsertest.cc
@@ -162,6 +162,8 @@
blink::features::kSubSampleWindowProxyUsageMetrics,
// PNA metrics may not record correctly if LNA checks are enabled.
network::features::kLocalNetworkAccessChecks,
+ // Disabling this flag just to test that the flag is working.
+ blink::features::kRemoveCharsetAutoDetectionForISO2022JP,
};
}
@@ -2999,7 +3001,6 @@
EXPECT_TRUE(content::NavigateToURL(
web_contents(), https_server().GetURL("/security/utf8.html")));
CheckCounter(WebFeature::kCharsetAutoDetection, 0);
- CheckCounter(WebFeature::kCharsetAutoDetectionISO2022JP, 0);
}
IN_PROC_BROWSER_TEST_F(ChromeWebPlatformSecurityMetricsBrowserTest,
@@ -3007,15 +3008,16 @@
EXPECT_TRUE(content::NavigateToURL(
web_contents(), https_server().GetURL("/security/no_charset.html")));
CheckCounter(WebFeature::kCharsetAutoDetection, 1);
- CheckCounter(WebFeature::kCharsetAutoDetectionISO2022JP, 0);
}
IN_PROC_BROWSER_TEST_F(ChromeWebPlatformSecurityMetricsBrowserTest,
ISO2022JPDetection) {
EXPECT_TRUE(content::NavigateToURL(
web_contents(), https_server().GetURL("/security/iso_2022_jp.html")));
- CheckCounter(WebFeature::kCharsetAutoDetection, 1);
- CheckCounter(WebFeature::kCharsetAutoDetectionISO2022JP, 1);
+ // Given RemoveCharsetAutoDetectionForISO2022JP is disabled in
+ // ChromeWebPlatformSecurityMetricsBrowserTest, this should pass.
+ EXPECT_EQ("ISO-2022-JP",
+ content::EvalJs(web_contents(), "document.characterSet"));
}
// TODO(arthursonzogni): Add basic test(s) for the WebFeatures:
diff --git a/third_party/blink/renderer/core/dom/decoded_data_document_parser.cc b/third_party/blink/renderer/core/dom/decoded_data_document_parser.cc
index 4d4c2e0..2f11166 100644
--- a/third_party/blink/renderer/core/dom/decoded_data_document_parser.cc
+++ b/third_party/blink/renderer/core/dom/decoded_data_document_parser.cc
@@ -66,10 +66,6 @@
String decoded = decoder_->Decode(bytes, &auto_detected_charset);
if (!auto_detected_charset.empty()) {
GetDocument()->CountUse(WebFeature::kCharsetAutoDetection);
- if (auto_detected_charset == "ISO-2022-JP") {
- GetDocument()->CountDeprecation(
- WebFeature::kCharsetAutoDetectionISO2022JP);
- }
}
UpdateDocument(decoded);
}
diff --git a/third_party/blink/renderer/core/frame/deprecation/deprecation.json5 b/third_party/blink/renderer/core/frame/deprecation/deprecation.json5
index ba3820c9..2a04e37 100644
--- a/third_party/blink/renderer/core/frame/deprecation/deprecation.json5
+++ b/third_party/blink/renderer/core/frame/deprecation/deprecation.json5
@@ -80,15 +80,6 @@
chrome_status_feature: 5735596811091968,
},
{
- name: "CharsetAutoDetectionISO2022JP",
- message: "Auto-detection of ISO-2022-JP character set is deprecated and it will be disabled in the near future. Please specify character set in the Content Type header or by using a meta tag (https://developer.mozilla.org/en-US/docs/Glossary/Character_encoding).",
- translation_note: "Warning displayed to developers that they did not specify character encoding for HTML document, and that auto-detection of specific character set (i.e. ISO-2022-JP) used by the webiste won't be supported in the near futre.",
- web_features: [
- "kCharsetAutoDetectionISO2022JP",
- ],
- chrome_status_feature: 6576566521561088,
- },
- {
name: "ChromeLoadTimesConnectionInfo",
message: "`chrome.loadTimes()` is deprecated, instead use standardized API: Navigation Timing 2.",
translation_note: "This warning occurs when the website attempts to invoke the deprecated `chrome.loadTimes().connectionInfo` API.",
diff --git a/third_party/blink/renderer/core/html/parser/text_resource_decoder.cc b/third_party/blink/renderer/core/html/parser/text_resource_decoder.cc
index 8d8731b4..a7620ee5 100644
--- a/third_party/blink/renderer/core/html/parser/text_resource_decoder.cc
+++ b/third_party/blink/renderer/core/html/parser/text_resource_decoder.cc
@@ -28,6 +28,7 @@
#include "base/strings/string_view_util.h"
#include "base/trace_event/trace_event.h"
#include "third_party/blink/renderer/core/html/parser/html_meta_charset_parser.h"
+#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
#include "third_party/blink/renderer/platform/text/text_encoding_detector.h"
#include "third_party/blink/renderer/platform/wtf/text/string_view.h"
#include "third_party/blink/renderer/platform/wtf/text/text_codec.h"
@@ -342,14 +343,21 @@
if (DetectTextEncoding(
base::as_bytes(data), options_.HintEncoding().Utf8().c_str(),
options_.HintURL(), options_.HintLanguage(), &detected_encoding)) {
- SetEncoding(detected_encoding, kEncodingFromContentSniffing);
- if (auto_detected_charset != nullptr &&
- (options_.GetContentType() ==
- TextResourceDecoderOptions::kHTMLContent ||
- options_.GetContentType() ==
- TextResourceDecoderOptions::kXMLContent)) {
- *auto_detected_charset = detected_encoding.GetName();
+ if (options_.GetContentType() == TextResourceDecoderOptions::kHTMLContent ||
+ options_.GetContentType() == TextResourceDecoderOptions::kXMLContent) {
+ const AtomicString& charset = detected_encoding.GetName();
+ if (charset == "ISO-2022-JP" &&
+ RuntimeEnabledFeatures::
+ RemoveCharsetAutoDetectionForISO2022JPEnabled()) {
+ // Auto-detection of ISO-2022-JP is disabled: crbug.com/40089450
+ return;
+ }
+
+ if (auto_detected_charset != nullptr) {
+ *auto_detected_charset = charset;
+ }
}
+ SetEncoding(detected_encoding, kEncodingFromContentSniffing);
}
if (detected_encoding != UnknownEncoding()) {
detection_completed_ = true;
diff --git a/third_party/blink/renderer/core/html/parser/text_resource_decoder_test.cc b/third_party/blink/renderer/core/html/parser/text_resource_decoder_test.cc
index e6003eed..b43b680 100644
--- a/third_party/blink/renderer/core/html/parser/text_resource_decoder_test.cc
+++ b/third_party/blink/renderer/core/html/parser/text_resource_decoder_test.cc
@@ -185,4 +185,19 @@
EXPECT_EQ(Utf8Encoding(), decoder->Encoding());
}
+TEST(TextResourceDecoderTest, DoNotAutoDetectISO2022JP) {
+ test::TaskEnvironment task_environment;
+ std::unique_ptr<TextResourceDecoder> decoder =
+ std::make_unique<TextResourceDecoder>(
+ TextResourceDecoderOptions::CreateWithAutoDetection(
+ TextResourceDecoderOptions::kHTMLContent, Utf8Encoding(),
+ UnknownEncoding(), NullURL()));
+ // ISO-2022-JP escape sequences.
+ const unsigned char kISO2022JP[] = {0x1b, 0x24, 0x42, 0x30, 0x42,
+ 0x30, 0x44, 0x1b, 0x28, 0x42};
+ String auto_detected_charset;
+ decoder->Decode(base::span(kISO2022JP), &auto_detected_charset);
+ EXPECT_TRUE(auto_detected_charset.empty());
+}
+
} // namespace blink
diff --git a/third_party/blink/renderer/platform/runtime_enabled_features.json5 b/third_party/blink/renderer/platform/runtime_enabled_features.json5
index f3e26ce..50016912 100644
--- a/third_party/blink/renderer/platform/runtime_enabled_features.json5
+++ b/third_party/blink/renderer/platform/runtime_enabled_features.json5
@@ -3969,6 +3969,13 @@
"default": "experimental"},
},
{
+ // This feature acts as a kill switch for ISO-2022-JP auto-detection.
+ // This will be shipped in M139, and it can be removed in M141.
+ // See https://chromestatus.com/feature/6576566521561088.
+ name: "RemoveCharsetAutoDetectionForISO2022JP",
+ status: "stable",
+ },
+ {
name: "RemoveDanglingMarkupInTarget",
status: "stable",
},
diff --git a/third_party/blink/web_tests/external/wpt/encoding-detection/ja-ISO-2022-JP-late.tentative-expected.txt b/third_party/blink/web_tests/external/wpt/encoding-detection/ja-ISO-2022-JP-late.tentative-expected.txt
new file mode 100644
index 0000000..a697926
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/encoding-detection/ja-ISO-2022-JP-late.tentative-expected.txt
@@ -0,0 +1,4 @@
+This is a testharness.js-based test.
+[FAIL] Check detection result
+ assert_equals: Expected ISO-2022-JP expected "ISO-2022-JP" but got "windows-1252"
+Harness: the test ran to completion.
diff --git a/third_party/blink/web_tests/external/wpt/encoding-detection/ja-ISO-2022-JP.tentative-expected.txt b/third_party/blink/web_tests/external/wpt/encoding-detection/ja-ISO-2022-JP.tentative-expected.txt
new file mode 100644
Regression Test / PoC
diff --git a/chrome/browser/chrome_web_platform_security_metrics_browsertest.cc b/chrome/browser/chrome_web_platform_security_metrics_browsertest.cc
index 531f43f..81bb210 100644
--- a/chrome/browser/chrome_web_platform_security_metrics_browsertest.cc
+++ b/chrome/browser/chrome_web_platform_security_metrics_browsertest.cc
@@ -162,6 +162,8 @@
blink::features::kSubSampleWindowProxyUsageMetrics,
// PNA metrics may not record correctly if LNA checks are enabled.
network::features::kLocalNetworkAccessChecks,
+ // Disabling this flag just to test that the flag is working.
+ blink::features::kRemoveCharsetAutoDetectionForISO2022JP,
};
}
@@ -2999,7 +3001,6 @@
EXPECT_TRUE(content::NavigateToURL(
web_contents(), https_server().GetURL("/security/utf8.html")));
CheckCounter(WebFeature::kCharsetAutoDetection, 0);
- CheckCounter(WebFeature::kCharsetAutoDetectionISO2022JP, 0);
}
IN_PROC_BROWSER_TEST_F(ChromeWebPlatformSecurityMetricsBrowserTest,
@@ -3007,15 +3008,16 @@
EXPECT_TRUE(content::NavigateToURL(
web_contents(), https_server().GetURL("/security/no_charset.html")));
CheckCounter(WebFeature::kCharsetAutoDetection, 1);
- CheckCounter(WebFeature::kCharsetAutoDetectionISO2022JP, 0);
}
IN_PROC_BROWSER_TEST_F(ChromeWebPlatformSecurityMetricsBrowserTest,
ISO2022JPDetection) {
EXPECT_TRUE(content::NavigateToURL(
web_contents(), https_server().GetURL("/security/iso_2022_jp.html")));
- CheckCounter(WebFeature::kCharsetAutoDetection, 1);
- CheckCounter(WebFeature::kCharsetAutoDetectionISO2022JP, 1);
+ // Given RemoveCharsetAutoDetectionForISO2022JP is disabled in
+ // ChromeWebPlatformSecurityMetricsBrowserTest, this should pass.
+ EXPECT_EQ("ISO-2022-JP",
+ content::EvalJs(web_contents(), "document.characterSet"));
}
// TODO(arthursonzogni): Add basic test(s) for the WebFeatures:
diff --git a/third_party/blink/renderer/core/html/parser/text_resource_decoder_test.cc b/third_party/blink/renderer/core/html/parser/text_resource_decoder_test.cc
index e6003eed..b43b680 100644
--- a/third_party/blink/renderer/core/html/parser/text_resource_decoder_test.cc
+++ b/third_party/blink/renderer/core/html/parser/text_resource_decoder_test.cc
@@ -185,4 +185,19 @@
EXPECT_EQ(Utf8Encoding(), decoder->Encoding());
}
+TEST(TextResourceDecoderTest, DoNotAutoDetectISO2022JP) {
+ test::TaskEnvironment task_environment;
+ std::unique_ptr<TextResourceDecoder> decoder =
+ std::make_unique<TextResourceDecoder>(
+ TextResourceDecoderOptions::CreateWithAutoDetection(
+ TextResourceDecoderOptions::kHTMLContent, Utf8Encoding(),
+ UnknownEncoding(), NullURL()));
+ // ISO-2022-JP escape sequences.
+ const unsigned char kISO2022JP[] = {0x1b, 0x24, 0x42, 0x30, 0x42,
+ 0x30, 0x44, 0x1b, 0x28, 0x42};
+ String auto_detected_charset;
+ decoder->Decode(base::span(kISO2022JP), &auto_detected_charset);
+ EXPECT_TRUE(auto_detected_charset.empty());
+}
+
} // namespace blink
diff --git a/third_party/blink/web_tests/external/wpt/encoding-detection/ja-ISO-2022-JP-late.tentative-expected.txt b/third_party/blink/web_tests/external/wpt/encoding-detection/ja-ISO-2022-JP-late.tentative-expected.txt
new file mode 100644
index 0000000..a697926
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/encoding-detection/ja-ISO-2022-JP-late.tentative-expected.txt
@@ -0,0 +1,4 @@
+This is a testharness.js-based test.
+[FAIL] Check detection result
+ assert_equals: Expected ISO-2022-JP expected "ISO-2022-JP" but got "windows-1252"
+Harness: the test ran to completion.
diff --git a/third_party/blink/web_tests/external/wpt/encoding-detection/ja-ISO-2022-JP.tentative-expected.txt b/third_party/blink/web_tests/external/wpt/encoding-detection/ja-ISO-2022-JP.tentative-expected.txt
new file mode 100644
index 0000000..a697926
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/encoding-detection/ja-ISO-2022-JP.tentative-expected.txt
@@ -0,0 +1,4 @@
+This is a testharness.js-based test.
+[FAIL] Check detection result
+ assert_equals: Expected ISO-2022-JP expected "ISO-2022-JP" but got "windows-1252"
+Harness: the test ran to completion.
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/issues/iso-2022-jp-auto-detection-expected.txt b/third_party/blink/web_tests/http/tests/inspector-protocol/issues/iso-2022-jp-auto-detection-expected.txt
deleted file mode 100644
index 80c2957..0000000
--- a/third_party/blink/web_tests/http/tests/inspector-protocol/issues/iso-2022-jp-auto-detection-expected.txt
+++ /dev/null
@@ -1,20 +0,0 @@
-Tests that deprecation issues for ISO-2022-JP are reported
-Inspector issue: {
- issue : {
- code : DeprecationIssue
- details : {
- deprecationIssueDetails : {
- affectedFrame : {
- frameId : <string>
- }
- sourceCodeLocation : {
- columnNumber : 0
- lineNumber : 0
- url : https://example.test:8443/inspector-protocol/resources/iso_2022_jp.html
- }
- type : CharsetAutoDetectionISO2022JP
- }
- }
- }
-}
-
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/issues/iso-2022-jp-auto-detection.js b/third_party/blink/web_tests/http/tests/inspector-protocol/issues/iso-2022-jp-auto-detection.js
deleted file mode 100644
index 259458b..0000000
--- a/third_party/blink/web_tests/http/tests/inspector-protocol/issues/iso-2022-jp-auto-detection.js
+++ /dev/null
@@ -1,13 +0,0 @@
-(async testRunner => {
- const {page, session, dp} = await testRunner.startBlank(
- `Tests that deprecation issues for ISO-2022-JP are reported`);
- await dp.Audits.enable();
- const promise = dp.Audits.onceIssueAdded();
-
- page.navigate(
- 'https://example.test:8443/inspector-protocol/resources/iso_2022_jp.html');
-
- const result = await promise;
- testRunner.log(result.params, "Inspector issue: ");
- testRunner.completeTest();
- });
\ No newline at end of file
diff --git a/third_party/blink/web_tests/http/tests/inspector-protocol/resources/iso_2022_jp.html b/third_party/blink/web_tests/http/tests/inspector-protocol/resources/iso_2022_jp.html
deleted file mode 100644
index f86a7cb..0000000
--- a/third_party/blink/web_tests/http/tests/inspector-protocol/resources/iso_2022_jp.html
+++ /dev/null
@@ -1,7 +0,0 @@
-<!DOCTYPE html>
-<html><head>
-<title>XSS</title>
-</head>
-<body>
-<img alt="$@">(B <img alt="id=foo ">
-</body></html>
\ No newline at end of file
Original Bug Report
Security: ASCII can be autodetected as ISO-2022-JP
VULNERABILITY DETAILS
All-ASCII HTML can get autodetected as ISO-2022-JP, which makes ASCII content that doesn’t look like markup treated as active content. This may lead to XSS if the site serves user-supplied content.
As a mitigating factor, to be vulnerable a site has to violate two best practices:
- The site needs not to declare its character encoding. (Pretty realistic, unfortunately.)
- Instead of escaping all less-than signs in user-supplied content as would be proper, the site needs to try to filter out markup specifically. (The proof of concept includes an ASCII less-than sign byte followed by a non-alphabetic byte, so there’s a chance there are filter out there that wouldn’t consider it as matching a regexp for an HTML tag.)
However, from the work I’ve seen Mike West do, I have inferred that Chrome considers these kinds of site mistakes to be in scope for browser-based counter-measures.
As a remedy, I suggest excluding ISO-2022-JP from potential autodetection outcomes.
VERSION Google Chrome 62.0.3202.75 (Official Build) (64-bit) Revision 67b212ffb03c4401235f8961e2d15371b96cde27-refs/branch-heads/3202@{#750} OS Ubuntu 16.04
REPRODUCTION CASE Load https://hsivonen.com/test/p/iso-2022-jp-autodetect.htm in Chrome.
ADDITIONAL INFO For bug unhiding, coordination with unhiding https://bugzilla.mozilla.org/show_bug.cgi?id=1362365 would be appreciated.