CVE-2026-17843
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fthird_party/blink/renderer/core/css/css_image_value_test.cc |
modified | |
ifthird_party/blink/renderer/core/css/css_image_value_test.cc |
modified |
Files Changed
third_party/blink/renderer/core/css/css_image_value_test.ccthird_party/blink/renderer/core/css/css_url_data.ccthird_party/blink/web_tests/wpt_internal/css/css-values/urls/base-change-dangling-href.html
Patch
From 73a3f1d8f0cb4fb72db53ca351f111d339f9f5b0 Mon Sep 17 00:00:00 2001 From: Kevin Babbitt <[email protected]> Date: Wed, 03 Jun 2026 05:53:12 -0700 Subject: [PATCH] Preserve original CSS URL when generating computed value Similar fix to https://crrev.com/c/7859244 in a different spot to maintain the dangling markup mitigation. Fixed: 518103887 Change-Id: Id77b349b2c8901602a6c6b0a5421e12f047b8845 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7895140 Reviewed-by: Rune Lillesveen <[email protected]> Commit-Queue: Kevin Babbitt <[email protected]> Cr-Commit-Position: refs/heads/main@{#1640871} --- diff --git a/third_party/blink/renderer/core/css/css_image_value_test.cc b/third_party/blink/renderer/core/css/css_image_value_test.cc index 85afbde3..3f12c03 100644 --- a/third_party/blink/renderer/core/css/css_image_value_test.cc +++ b/third_party/blink/renderer/core/css/css_image_value_test.cc @@ -149,4 +149,56 @@ } } +// Ensure that CSSUrlData::MakeComputed() does not launder the dangling markup +// flag by overwriting relative_url_ with the canonicalized absolute URL. +// The attack vector: style resolution creates a computed value via +// MakeComputed(), then commitStyles() serializes it (via CssText(), which +// uses relative_url_) and re-parses it on the element's inline style. If +// relative_url_ was overwritten with the canonical form, the re-parsed URL +// lacks newlines and the dangling markup flag is lost. +TEST_F(CSSImageValueTest, MakeComputedDoesNotLaunderDanglingMarkup) { + SimRequest main_resource("https://example.com/index.html", "text/html"); + + LoadURL("https://example.com/index.html"); + + main_resource.Complete(R"HTML( + <!doctype html> + <style> + @keyframes anim { + from { background-image: url('/exfil?\a <secret'); } + to { background-image: url('/exfil?\a <secret'); } + } + #target { + animation: anim 1s paused; + width: 100px; + height: 100px; + } + </style> + <div id="target"></div> + )HTML"); + + test::RunPendingTasks(); + GetDocument().UpdateStyleAndLayoutTree(); + Compositor().BeginFrame(); + + // commitStyles() serializes the computed animation style (which goes + // through MakeComputed()) and sets it on the inline style. If the fix + // is missing, the re-parsed URL loses the dangling markup flag. + MainFrame().ExecuteScript(WebScriptSource(R"JS( + const target = document.getElementById('target'); + target.getAnimations()[0].commitStyles(); + )JS")); + + GetDocument().UpdateStyleAndLayoutTree(); + Compositor().BeginFrame(); + + auto* target = GetDocument().getElementById(AtomicString("target")); + ASSERT_TRUE(target); + const StyleImage* image = + target->ComputedStyleRef().BackgroundLayers().GetImage(); + if (image) { + EXPECT_TRUE(image->ErrorOccurred()); + } +} + } // namespace blink diff --git a/third_party/blink/renderer/core/css/css_url_data.cc b/third_party/blink/renderer/core/css/css_url_data.cc index 3788e0a..fd87cc0 100644 --- a/third_party/blink/renderer/core/css/css_url_data.cc +++ b/third_party/blink/renderer/core/css/css_url_data.cc @@ -167,10 +167,16 @@ if (relative_url_.empty() || is_local_ || absolute_url_.empty()) { return this; } + // When the URL was flagged as potentially dangling markup, keep the raw + // relative spelling so that CssText() round-trips a string that re-derives + // PotentiallyDanglingMarkup() on the consuming side (e.g. via + // commitStyles()). Using the canonicalized serialization would launder away + // the newline/'<' characters. This matches the logic in MakeResolved(). return MakeGarbageCollected<CSSUrlData>( - base::PassKey<CSSUrlData>(), absolute_url_, absolute_url_, Referrer(), - is_from_origin_clean_style_sheet_, is_ad_related_, is_local_, - potentially_dangling_markup_, modifiers_); + base::PassKey<CSSUrlData>(), + potentially_dangling_markup_ ? relative_url_ : absolute_url_, + absolute_url_, Referrer(), is_from_origin_clean_style_sheet_, + is_ad_related_, is_local_, potentially_dangling_markup_, modifiers_); } const CSSUrlData* CSSUrlData::MakeResolved(const KURL& base_url, diff --git a/third_party/blink/web_tests/wpt_internal/css/css-values/urls/base-change-dangling-href.html b/third_party/blink/web_tests/wpt_internal/css/css-values/urls/base-change-dangling-href.html index 2464b48..b24f2c9 100644 --- a/third_party/blink/web_tests/wpt_internal/css/css-values/urls/base-change-dangling-href.html +++ b/third_party/blink/web_tests/wpt_internal/css/css-values/urls/base-change-dangling-href.html @@ -8,10 +8,15 @@ <div id="target"></div> <script> test(() => { - const url_without_base = `url("${new URL("img.gif", location.href)}")`; target.style.listStyleImage = "url(img.gif)"; base_elm.remove(); - assert_equals(getComputedStyle(target).listStyleImage, url_without_base, - "The url is re-resolved after the base change"); + // The base element had a dangling markup href (containing newline and '<'). + // Because potentially_dangling_markup is set, the raw relative URL is + // preserved through MakeComputed() to prevent laundering the dangling + // markup flag during serialization/re-parse cycles (e.g. commitStyles()). + // This is a deliberate deviation from CSS Values §4.5.1 (which says + // computed url() should be absolute) for this security edge case. + assert_equals(getComputedStyle(target).listStyleImage, 'url("img.gif")', + "The dangling-markup url preserves the relative form"); }, "Removing the base element should not crash."); </script>
Regression Test / PoC
diff --git a/third_party/blink/renderer/core/css/css_image_value_test.cc b/third_party/blink/renderer/core/css/css_image_value_test.cc
index 85afbde3..3f12c03 100644
--- a/third_party/blink/renderer/core/css/css_image_value_test.cc
+++ b/third_party/blink/renderer/core/css/css_image_value_test.cc
@@ -149,4 +149,56 @@
}
}
+// Ensure that CSSUrlData::MakeComputed() does not launder the dangling markup
+// flag by overwriting relative_url_ with the canonicalized absolute URL.
+// The attack vector: style resolution creates a computed value via
+// MakeComputed(), then commitStyles() serializes it (via CssText(), which
+// uses relative_url_) and re-parses it on the element's inline style. If
+// relative_url_ was overwritten with the canonical form, the re-parsed URL
+// lacks newlines and the dangling markup flag is lost.
+TEST_F(CSSImageValueTest, MakeComputedDoesNotLaunderDanglingMarkup) {
+ SimRequest main_resource("https://example.com/index.html", "text/html");
+
+ LoadURL("https://example.com/index.html");
+
+ main_resource.Complete(R"HTML(
+ <!doctype html>
+ <style>
+ @keyframes anim {
+ from { background-image: url('/exfil?\a <secret'); }
+ to { background-image: url('/exfil?\a <secret'); }
+ }
+ #target {
+ animation: anim 1s paused;
+ width: 100px;
+ height: 100px;
+ }
+ </style>
+ <div id="target"></div>
+ )HTML");
+
+ test::RunPendingTasks();
+ GetDocument().UpdateStyleAndLayoutTree();
+ Compositor().BeginFrame();
+
+ // commitStyles() serializes the computed animation style (which goes
+ // through MakeComputed()) and sets it on the inline style. If the fix
+ // is missing, the re-parsed URL loses the dangling markup flag.
+ MainFrame().ExecuteScript(WebScriptSource(R"JS(
+ const target = document.getElementById('target');
+ target.getAnimations()[0].commitStyles();
+ )JS"));
+
+ GetDocument().UpdateStyleAndLayoutTree();
+ Compositor().BeginFrame();
+
+ auto* target = GetDocument().getElementById(AtomicString("target"));
+ ASSERT_TRUE(target);
+ const StyleImage* image =
+ target->ComputedStyleRef().BackgroundLayers().GetImage();
+ if (image) {
+ EXPECT_TRUE(image->ErrorOccurred());
+ }
+}
+
} // namespace blink
diff --git a/third_party/blink/web_tests/wpt_internal/css/css-values/urls/base-change-dangling-href.html b/third_party/blink/web_tests/wpt_internal/css/css-values/urls/base-change-dangling-href.html
index 2464b48..b24f2c9 100644
--- a/third_party/blink/web_tests/wpt_internal/css/css-values/urls/base-change-dangling-href.html
+++ b/third_party/blink/web_tests/wpt_internal/css/css-values/urls/base-change-dangling-href.html
@@ -8,10 +8,15 @@
<div id="target"></div>
<script>
test(() => {
- const url_without_base = `url("${new URL("img.gif", location.href)}")`;
target.style.listStyleImage = "url(img.gif)";
base_elm.remove();
- assert_equals(getComputedStyle(target).listStyleImage, url_without_base,
- "The url is re-resolved after the base change");
+ // The base element had a dangling markup href (containing newline and '<').
+ // Because potentially_dangling_markup is set, the raw relative URL is
+ // preserved through MakeComputed() to prevent laundering the dangling
+ // markup flag during serialization/re-parse cycles (e.g. commitStyles()).
+ // This is a deliberate deviation from CSS Values §4.5.1 (which says
+ // computed url() should be absolute) for this security edge case.
+ assert_equals(getComputedStyle(target).listStyleImage, 'url("img.gif")',
+ "The dangling-markup url preserves the relative form");
}, "Removing the base element should not crash.");
</script>
Original Bug Report
Bypass of dangling markup protection via CSSUrlData::MakeComputed
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 logical vulnerability in CSSUrlData::MakeComputed can potentially bypass Blink’s dangling markup subresource-blocking defense. When generating a computed CSS URL value, the raw relative URL is erroneously overwritten with the canonicalized absolute URL where newlines are stripped. Subsequent serialization and re-parsing cycles of this value can cause the subresource load to bypass the security block.
Affected files:
third_party/blink/renderer/core/css/css_url_data.cc
Estimated timestamp from git blame: 2025-05-12
Description
There is a potential logical bypass of Blink’s dangling markup subresource-blocking defense during the serialization and re-parsing of computed CSS URL values. When CSSUrlData::MakeComputed() clones a URL data instance for a computed style, it overwrites the relative_url_ (unresolved URL) with the canonicalized absolute_url_. Because URL canonicalization strips removable whitespace characters (such as \n, \r, and \t), the raw whitespace characters that originally triggered the dangling markup detection are lost. When this computed value is serialized and then parsed again by the browser, the new URL is not recognized as potentially dangling, allowing the subresource request to bypass the safety block.
Root Cause
In third_party/blink/renderer/core/css/css_url_data.cc:
const CSSUrlData* CSSUrlData::MakeComputed() const {
if (relative_url_.empty() || is_local_ || absolute_url_.empty()) {
return this;
}
return MakeGarbageCollected<CSSUrlData>(
base::PassKey<CSSUrlData>(), absolute_url_, absolute_url_, Referrer(),
is_from_origin_clean_style_sheet_, is_ad_related_, is_local_,
potentially_dangling_markup_, modifiers_);
}
Here, absolute_url_ is passed as both parameters 2 and 3 (the unresolved and resolved URL arguments). This overwrites the new object’s relative_url_ with the canonicalized absolute URL string.
When CssText() is called, it serializes using relative_url_:
String CSSUrlData::CssText() const {
return SerializeURI(relative_url_, modifiers_);
}
Because relative_url_ was overwritten, the returned string contains the canonical absolute URL (where the raw newlines are absent). If this serialized string is re-parsed, the URL parser’s whitespace checking code (in url/url_canon_etc.cc’s DoRemoveUrlWhitespace) will not flag the new URL as potentially dangling since it lacks raw whitespace characters. Thus, the dangling markup flag is lost on re-parsing.
Potential Trigger Steps
(Note: These are potential, theoretical steps as our tooling does not currently have the capability to run code to verify with a live proof of concept)
- An attacker injects a CSS URL value containing unclosed quotes and a newline followed by a
<character (e.g.,url('/exfil?\a <secret'), where\ais the hex escape for a newline). - On the initial parse,
potentially_dangling_markup_is set totrue. When Blink tries to load the resource, the fetch is blocked inBaseFetchContext::CanRequestInternal()because the URL’sPotentiallyDanglingMarkup()flag istrue. - A script or layout operation triggers style resolution, creating a computed CSS value.
CSSImageValue::ComputedCSSValue()callsurl_data_->MakeComputed(), which incorrectly overwritesrelative_url_with the canonicalizedabsolute_url_(removing the newline). - The application triggers style serialization and re-parsing. For example, calling
animation.commitStyles()serializes the computed style viaCssText()and immediately sets it back on the element’s inline style attribute. - The serialized CSS contains the canonicalized absolute URL string which has no raw newlines.
- Upon re-parsing, the new
KURLis constructed withPotentiallyDanglingMarkup()set tofalse. - Blink fetches the image resource successfully without blocking, potentially exfiltrating the captured page bytes.
Suggested Fix
Modify CSSUrlData::MakeComputed() to preserve the raw unresolved relative_url_ when potentially_dangling_markup_ is true, matching the logic implemented in MakeResolved():
const CSSUrlData* CSSUrlData::MakeComputed() const {
if (relative_url_.empty() || is_local_ || absolute_url_.empty()) {
return this;
}
return MakeGarbageCollected<CSSUrlData>(
base::PassKey<CSSUrlData>(),
potentially_dangling_markup_ ? relative_url_ : absolute_url_,
absolute_url_, Referrer(), is_from_origin_clean_style_sheet_,
is_ad_related_, is_local_, potentially_dangling_markup_, modifiers_);
}
Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040
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.