CVE-2026-14147
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/css/link_condition.cc |
modified | |
ifthird_party/blink/renderer/core/css/navigation_query.cc |
modified |
Files Changed
third_party/blink/renderer/core/css/link_condition.ccthird_party/blink/renderer/core/css/navigation_query.ccthird_party/blink/web_tests/wpt_internal/route/link-to-serialization.htmlthird_party/blink/web_tests/wpt_internal/route/parsing/serialize-001.html
Patch
From 32d8d23881ec623ebd8a52a5cfbc04f723b656c8 Mon Sep 17 00:00:00 2001 From: Rune Lillesveen <[email protected]> Date: Wed, 20 May 2026 05:02:18 -0700 Subject: [PATCH] Escape strings and idents for serialization of routes - Escape string in url-pattern() - Escape strings and idents in @navigation conditionals Bug: 514632767 Change-Id: Iac5fcbfa08e3b65ecbdaaa0945b2bacc39c8ddfd Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7864102 Commit-Queue: Rune Lillesveen <[email protected]> Reviewed-by: Morten Stenshorne <[email protected]> Commit-Queue: Morten Stenshorne <[email protected]> Cr-Commit-Position: refs/heads/main@{#1633516} --- diff --git a/third_party/blink/renderer/core/css/link_condition.cc b/third_party/blink/renderer/core/css/link_condition.cc index b70e7913..6933ce4 100644 --- a/third_party/blink/renderer/core/css/link_condition.cc +++ b/third_party/blink/renderer/core/css/link_condition.cc @@ -5,6 +5,7 @@ #include "third_party/blink/renderer/core/css/link_condition.h" #include "base/memory/stack_allocated.h" +#include "third_party/blink/renderer/core/css/css_markup.h" #include "third_party/blink/renderer/core/css/navigation_query.h" #include "third_party/blink/renderer/core/dom/document.h" #include "third_party/blink/renderer/core/html/html_anchor_element.h" @@ -95,15 +96,13 @@ void NavigationParamExpNode::SerializeTo(StringBuilder& builder) const { if (value_) { - builder.Append("\""); - builder.Append(param_); - builder.Append("\": \""); - builder.Append(value_); - builder.Append("\""); + SerializeString(param_, builder); + builder.Append(": "); + SerializeString(value_, builder); } else { - builder.Append("navigation-param(\""); - builder.Append(param_); - builder.Append("\")"); + builder.Append("navigation-param("); + SerializeString(param_, builder); + builder.Append(")"); } } diff --git a/third_party/blink/renderer/core/css/navigation_query.cc b/third_party/blink/renderer/core/css/navigation_query.cc index ce5936bb..d069fb1 100644 --- a/third_party/blink/renderer/core/css/navigation_query.cc +++ b/third_party/blink/renderer/core/css/navigation_query.cc @@ -4,6 +4,7 @@ #include "third_party/blink/renderer/core/css/navigation_query.h" +#include "third_party/blink/renderer/core/css/css_markup.h" #include "third_party/blink/renderer/core/dom/document.h" #include "third_party/blink/renderer/core/route_matching/route.h" #include "third_party/blink/renderer/core/route_matching/route_map.h" @@ -35,11 +36,11 @@ void NavigationLocation::SerializeTo(StringBuilder& builder) const { DCHECK(!string_.IsNull()); if (url_pattern_) { - builder.Append("url-pattern(\""); - builder.Append(string_); - builder.Append("\")"); + builder.Append("url-pattern("); + SerializeString(string_, builder); + builder.Append(")"); } else { - builder.Append(string_); + SerializeIdentifier(string_, builder); } } diff --git a/third_party/blink/web_tests/wpt_internal/route/link-to-serialization.html b/third_party/blink/web_tests/wpt_internal/route/link-to-serialization.html new file mode 100644 index 0000000..725a62b --- /dev/null +++ b/third_party/blink/web_tests/wpt_internal/route/link-to-serialization.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<title>:link-to() parsing tests</title> +<link rel="help" href="https://drafts.csswg.org/css-navigation-1/#link-to-pseudo-class"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<style id="sheet"> + :link-to(url-pattern("/\"")) {} +</style> +<script> + // Ideally, these tests should use test_valid_selector() from + // /css/support/parsing-testcommon.js instead, but that's crashing because + // of crbug.com/514721936 + test(() => { + sheet.sheet.cssRules.length; + assert_equals(sheet.sheet.cssRules.length, 1); + assert_equals(sheet.sheet.cssRules[0].selectorText, ':link-to(url-pattern("/\\""))'); + }, "Check that url-pattern() escapes the string on serialization"); +</script> diff --git a/third_party/blink/web_tests/wpt_internal/route/parsing/serialize-001.html b/third_party/blink/web_tests/wpt_internal/route/parsing/serialize-001.html index a006548b6..a5bc1967 100644 --- a/third_party/blink/web_tests/wpt_internal/route/parsing/serialize-001.html +++ b/third_party/blink/web_tests/wpt_internal/route/parsing/serialize-001.html @@ -17,6 +17,9 @@ <style class="test" id="sheet5"> @navigation (at: urlpattern("/r66/")) { } </style> +<style class="test" id="sheet6"> + @navigation (at: \)) { } +</style> <script src="/resources/testharness.js"></script> <script src="/resources/testharnessreport.js"></script> <script> @@ -25,6 +28,7 @@ '@navigation ((from: r66) or (to: blah))', '@navigation (Attention! This is General Enclosed speaking)', '@navigation (at: urlpattern("/r66/"))', + '@navigation (at: \\))', ]; document.querySelectorAll(".test").forEach((elm) => { test(()=> {
Regression Test / PoC
diff --git a/third_party/blink/web_tests/wpt_internal/route/link-to-serialization.html b/third_party/blink/web_tests/wpt_internal/route/link-to-serialization.html
new file mode 100644
index 0000000..725a62b
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/route/link-to-serialization.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<title>:link-to() parsing tests</title>
+<link rel="help" href="https://drafts.csswg.org/css-navigation-1/#link-to-pseudo-class">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style id="sheet">
+ :link-to(url-pattern("/\"")) {}
+</style>
+<script>
+ // Ideally, these tests should use test_valid_selector() from
+ // /css/support/parsing-testcommon.js instead, but that's crashing because
+ // of crbug.com/514721936
+ test(() => {
+ sheet.sheet.cssRules.length;
+ assert_equals(sheet.sheet.cssRules.length, 1);
+ assert_equals(sheet.sheet.cssRules[0].selectorText, ':link-to(url-pattern("/\\""))');
+ }, "Check that url-pattern() escapes the string on serialization");
+</script>
diff --git a/third_party/blink/web_tests/wpt_internal/route/parsing/serialize-001.html b/third_party/blink/web_tests/wpt_internal/route/parsing/serialize-001.html
index a006548b6..a5bc1967 100644
--- a/third_party/blink/web_tests/wpt_internal/route/parsing/serialize-001.html
+++ b/third_party/blink/web_tests/wpt_internal/route/parsing/serialize-001.html
@@ -17,6 +17,9 @@
<style class="test" id="sheet5">
@navigation (at: urlpattern("/r66/")) { }
</style>
+<style class="test" id="sheet6">
+ @navigation (at: \)) { }
+</style>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script>
@@ -25,6 +28,7 @@
'@navigation ((from: r66) or (to: blah))',
'@navigation (Attention! This is General Enclosed speaking)',
'@navigation (at: urlpattern("/r66/"))',
+ '@navigation (at: \\))',
];
document.querySelectorAll(".test").forEach((elm) => {
test(()=> {
Original Bug Report
Potential CSS Injection / mXSS via unescaped serialization in RouteMatching
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: The experimental RouteMatching feature fails to properly escape internal strings during CSS serialization for the :link-to() pseudo-class and @navigation rules. Specifically, NavigationParamExpNode and NavigationLocation append raw strings rather than using standard serialization helpers. If a web application reflects the serialized .cssText, an attacker can bypass sanitizers to inject arbitrary CSS.
Affected files:
third_party/blink/renderer/core/css/link_condition.ccthird_party/blink/renderer/core/css/navigation_query.cc
Estimated timestamp from git blame: 2025-11-04
Summary
There is a potential CSS mutation Cross-Site Scripting (mXSS) vulnerability in the experimental RouteMatching feature. When serializing the :link-to() pseudo-class and @navigation rules, Blink fails to properly escape internal strings (such as parameter names, values, and locations) before appending them to the CSS output.
Technical Details
The vulnerability resides in the manual string concatenation within the SerializeTo methods for NavigationParamExpNode and NavigationLocation. Instead of utilizing standard CSS serialization helpers like SerializeIdentifier or SerializeString (which safely escape quotes, slashes, and structural characters), the code directly appends the decoded AtomicString members into a StringBuilder.
In third_party/blink/renderer/core/css/link_condition.cc (NavigationParamExpNode::SerializeTo):
void NavigationParamExpNode::SerializeTo(StringBuilder& builder) const {
if (value_) {
builder.Append("\"");
builder.Append(param_); // <--- Appends raw, decoded string
builder.Append("\": \"");
builder.Append(value_); // <--- Appends raw, decoded string
builder.Append("\"");
} else {
// ...
}
}
A similar issue exists in NavigationLocation::SerializeTo in third_party/blink/renderer/core/css/navigation_query.cc, where string_ is appended directly.
Potential Exploitation Steps
Note: Our tooling agent does not have the capability to run code; the following are suggested/potential steps an attacker would use to trigger this vulnerability based on source code analysis.
- Payload Delivery: An attacker supplies a crafted CSS payload to a vulnerable web application that parses and later reflects CSS rules using the browser’s CSSOM (e.g., a CSS sanitizer or style editor).
Payload:
a:link-to(url(x) with foo\22 \3b \7d * \7b color: red; \7d \22 : \22 bar\22 ) { } - Decoding: During initial parsing, the CSS Lexer decodes escape sequences (like
\22for"and\7dfor}). TheNavigationParamExpNodestores the literal, decoded string:foo"; } * { color: red; } ". - Serialization: The web application reads the parsed rule back out via
rule.cssText. TheSerializeTomethod emits the raw string without re-escaping it. Serialized Output:a:link-to(url("x") with "foo"; } * { color: red; } ": "bar") { } - Execution (mXSS): The application injects this serialized string back into a
<style>block. The first literal}prematurely closes the:link-to()rule, and the subsequent* { color: red; }is parsed and executed as a new, attacker-controlled CSS rule.
Impact
This flaw allows attackers to perform CSS injection (mXSS) against applications that rely on the browser’s CSSOM serialization to sanitize or rewrite CSS. This feature is currently gated behind the experimental RouteMatching flag, limiting immediate production impact.
Suggested Fix
Refactor NavigationParamExpNode::SerializeTo and NavigationLocation::SerializeTo to use standard serialization helpers. Replace direct builder.Append(param_), builder.Append(value_), and builder.Append(string_) calls with SerializeString(param_, builder) or SerializeIdentifier(param_, builder) as appropriate for the CSS grammar context.
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.