Low watch CVSS 4.3 chrome Policy Bypass 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in DevTools
DescriptionInsufficient policy enforcement in DevTools
ComponentDevTools
Sub-componentConsole
Bug ClassPolicy Bypass
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.
Tracker474670215
Fix commita98136b97706 (devtools/devtools-frontend) +63/-5
CWECWE-602
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedLyra Rebane (rebane2001)
Reported2026-01-10 · bounty $1000 (per vendor advisory)
Fix landed2026-01-27 (public git)
Disclosed2026-03-10
Patch gap42 days — fix public in source before the advisory

Background

`%c` console formatter
DevTools lets console.log('%c...', css) style a console message. The frontend applies only a filtered subset of properties (ALLOWED_PROPERTY_PREFIXES such as background, border, color) through updateStyle in ConsoleFormat.ts.
Data-URL-only policy
the formatter’s own comment says it only allows data: URLs inside url(). Any property whose value contains a non-data: URL is skipped, so console styling cannot make DevTools load remote resources.
CSS escapes and `image-set()`
CSS identifiers and function names are case-insensitive and may be written with backslash or hex escapes (e.g. ur\6c (). image-set() can also take bare string URLs without url().
`if()` and unparsed values
when a value contains a substitution function such as if(), getPropertyValue returns the author’s raw token text (escapes and casing intact) rather than a normalized serialization, as the new test cases show.

Root Cause Analysis

updateStyle writes each declaration into a scratch element (buffer.style), reads it back with getPropertyValue, and skips the property if any URL captured by URL_REGEX is not a data: URL. Before the patch, URL_REGEX was /url\([\'\"]?([^\)]*)/g, which matches only lowercase, unescaped url(. Any URL reference spelled differently got past the check. Examples include urL(, ur\6c (, \u\r\l(, and an image-set() (escaped or not) that takes a bare string such as "http://localhost/a.png". The if(else:...) wrapper in the new tests keeps those spellings verbatim in the value that getPropertyValue returns, while the style engine still resolves them to a real image fetch.

The patch adds cssEscapeRegex, which builds a case-insensitive pattern that also accepts backslash and hex escapes, and uses it to rebuild URL_REGEX, now with the gi flags and a lookahead so overlapping matches are found. It also adds IMAGESET_REGEX/GOOD_IMAGESET_REGEX. If any image-set() contains an entry other than url("..."), type("...") or a resolution, the whole property is skipped with continue before the existing data: scheme check runs.

The fix tightens the filter: it rejects more inputs than before.

Key insight
The data:-only URL check relied on a literal, case-sensitive url( regex, but CSS allows URL references to be spelled in other ways: different casing, escape sequences, and bare strings inside image-set(). The fix makes the matcher escape-aware and case-insensitive, and rejects any image-set() that does not wrap every image in url().

Attack Path

  1. Page logs styled message A web page calls console.log with a %c style string whose allowed-prefix property (e.g. background-image) points to a remote URL.
  2. Disguise the URL reference The URL is written so the old URL_REGEX does not match it, for example if(else:ur\6c (http://...)), urL(...) or image-set("http://..." 2x), as in the new test cases.
  3. Filter accepts the property updateStyle finds no non-data: URL and keeps the property, so it is applied to the message element in the DevTools frontend.
  4. DevTools fetches the resource When the console renders the message, the DevTools frontend loads the attacker’s remote image. This lets the page learn that DevTools is open and inspecting it, along with the request metadata of that fetch (inferred; the diff shows only the filter bypass).

Impact Assessment

The vendor rates this low (“Insufficient policy enforcement in DevTools”), and the provided advisory data does not report in-the-wild exploitation. Ordinary web content in a stock build can supply the input, but the effect only occurs when the user has DevTools open and the console renders the page’s messages. The result is that the DevTools frontend loads attacker-chosen remote resources, against the formatter’s data:-only policy. Any consequence beyond that network fetch, such as DevTools detection or fingerprinting, is (inferred).

Files Changed

  • AUTHORS
  • front_end/panels/console/ConsoleFormat.test.ts
  • front_end/panels/console/ConsoleFormat.ts

Audit Directions

  • Regex-based CSS sanitizers
    Check other DevTools or WebUI code that filters CSS text with regexes (url(, expression, @import, image-set, src()) for case-sensitivity, escape handling, and URL-bearing functions they don’t cover.
  • Sanitize-after-serialize assumptions
    Code that validates the output of getPropertyValue/cssText assumes the value is normalized. Values containing substitution functions (if(), var(), attr()) keep raw tokens, so the check should run on parsed values, not on strings.
  • Allowlisted property prefixes with URL sinks
    Review every property allowed by ALLOWED_PROPERTY_PREFIXES (e.g. border-image-source, background, and font shorthands) for URL-taking syntaxes that the current matcher does not model.
From a98136b97706f7972cbc99b69e283398d90a442c Mon Sep 17 00:00:00 2001
From: Lyra Rebane <rebane2001@gmail.com>
Date: Thu, 22 Jan 2026 10:31:20 +0200
Subject: [PATCH] [console] Improve URL filtering for console.log %c formatter

This CL improves upon the URL regex of the %c formatter to prevent some
new `url()` and `image-set()` edge-cases in modern CSS. Specifically, it
covers cases where said CSS functions are escaped, and those where
`image-set()` is used without using `url()`;

The CL also adds the corresponding test cases.

Bug: 474670215
Change-Id: I21909a2650c985eac4f2ec714aea561e5d840003
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/7484749
Commit-Queue: Simon Zünd <szuend@chromium.org>
Reviewed-by: Simon Zünd <szuend@chromium.org>
Reviewed-by: Philip Pfaffe <pfaffe@chromium.org>
---

diff --git a/AUTHORS b/AUTHORS
index ff02b4a..5a301e4 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -64,6 +64,7 @@
 Liam DeBeasi <ldebeasi@gmail.com>
 Luke Swiderski <luke.swiderski@gmail.com>
 Luke Warlow <luke@warlow.dev>
+Lyra Rebane <rebane2001@gmail.com>
 Marijn Haverbeke <marijnh@gmail.com>
 Max 😎 Coplan <mchcopl@gmail.com>
 Michael Brüning <michael.bruning@qt.io>
diff --git a/front_end/panels/console/ConsoleFormat.test.ts b/front_end/panels/console/ConsoleFormat.test.ts
index 3a3b78a..4b963ed 100644
--- a/front_end/panels/console/ConsoleFormat.test.ts
+++ b/front_end/panels/console/ConsoleFormat.test.ts
@@ -444,19 +444,47 @@
           styles,
           'background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAAAAABzHgM7AAAAF0lEQVR42mM4Awb/wYCBYg6EgghRzAEAWDWBGQVyKPMAAAAASUVORK5CYII=), url(http://localhost/a.png)');  // Multiple URLs
       assert.isFalse(styles.has('background-image'));
+
+      Console.ConsoleFormat.updateStyle(
+          styles, 'background-image:if(supports():"url(data:";else:url(http://localhost/a.png))');
+      assert.isFalse(styles.has('background-image'));
+
+      Console.ConsoleFormat.updateStyle(styles, 'background-image:if(else:urL(http://localhost/a.png))');
+      assert.isFalse(styles.has('background-image'));
+
+      Console.ConsoleFormat.updateStyle(styles, 'background-image:if(else:ur\\6c (http://localhost/a.png))');
+      assert.isFalse(styles.has('background-image'));
+
+      Console.ConsoleFormat.updateStyle(styles, 'background-image:if(else:\\u\\r\\l(http://localhost/a.png))');
+      assert.isFalse(styles.has('background-image'));
+
+      Console.ConsoleFormat.updateStyle(
+          styles, 'background-image:if(else:image\\-set("data:" 1x, "http://localhost/a.png" 2x))');
+      assert.isFalse(styles.has('background-image'));
+
+      Console.ConsoleFormat.updateStyle(
+          styles, 'background-image:if(else:image-se\\74 ("data:" 1x, "http://localhost/a.png" 2x))');
+      assert.isFalse(styles.has('background-image'));
+
+      Console.ConsoleFormat.updateStyle(styles, 'background-image:image-set("data:" 1x, "http://localhost/a.png" 2x)');
+      assert.isFalse(styles.has('background-image'));
     });
 
     it('allows data urls in values', () => {
       const dataUrl =
-          'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAAAAABzHgM7AAAAF0lEQVR42mM4Awb/wYCBYg6EgghRzAEAWDWBGQVyKPMAAAAASUVORK5CYII=)';
+          'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAAAAABzHgM7AAAAF0lEQVR42mM4Awb/wYCBYg6EgghRzAEAWDWBGQVyKPMAAAAASUVORK5CYII=';
 
       const styles = new Map();
 
-      Console.ConsoleFormat.updateStyle(styles, `background-image:${dataUrl}`);
+      Console.ConsoleFormat.updateStyle(styles, `background-image:url(${dataUrl})`);
       assert.include(styles.get('background-image').value, 'data:image/png;base64');
 
-      Console.ConsoleFormat.updateStyle(styles, `border-image-source:${dataUrl}`);
+      Console.ConsoleFormat.updateStyle(styles, `border-image-source:url(${dataUrl})`);
       assert.include(styles.get('border-image-source').value, 'data:image/png;base64');
+
+      Console.ConsoleFormat.updateStyle(
+          styles, `background-image:image-set( "${dataUrl}" 1.5x , url("${dataUrl}") type( "image/png" ) )`);
+      assert.include(styles.get('background-image').value, 'data:image/png;base64');
     });
   });
 });
diff --git a/front_end/panels/console/ConsoleFormat.ts b/front_end/panels/console/ConsoleFormat.ts
index 3526590..bfc8b65 100644
--- a/front_end/panels/console/ConsoleFormat.ts
+++ b/front_end/panels/console/ConsoleFormat.ts
@@ -199,11 +199,34 @@
   return {tokens, args: args.slice(argIndex)};
 };
 
+/**
+ * This function converts a string into a partial regex string that
+ * case-insensitively matches it in CSS, even if CSS escapes are used.
+ *
+ * @param cssString the target string.
+ * @returns a partial regex matching the string in CSS.
+ */
+const cssEscapeRegex = (cssString: string): string => {
+  return [...cssString]
+      .map(char => {
+        const charCodes = new Set([char.toLowerCase(), char.toUpperCase()].map(c => c.charCodeAt(0).toString(16)));
+        const charCodeRegex =
+            [...charCodes].map(charCode => `\\\\0{0,${6 - charCode.length}}${charCode}[ \\n\\t]?`).join('|');
+        return `\\\\?(?:${charCodeRegex}|${char})`;
+      })
+      .join('');
+};
+
 export const updateStyle = (currentStyle: Map<string, {value: string, priority: string}>, styleToAdd: string): void => {
   const ALLOWED_PROPERTY_PREFIXES = ['background', 'border', 'color', 'font', 'line', 'margin', 'padding', 'text'];
   // We only allow data URLs with the `url()` CSS function.
   // The capture group is not intended to grab the whole URL exactly, just enough so we can check the scheme.
-  const URL_REGEX = /url\([\'\"]?([^\)]*)/g;
+  // The regex also covers CSS hex-escaped variations of `url()`.
+  const URL_REGEX = new RegExp(`(?=${cssEscapeRegex('url')}\\(['"]?([^\\)]*))`, 'gi');
+  // We greedily capture all `image-set()`s to make sure that all of
+  // them properly use `url()`s to enforce the data URL check later.
+  const IMAGESET_REGEX = new RegExp(`(?=(${cssEscapeRegex('image-set')}\\(.*))`, 'gi');
+  const GOOD_IMAGESET_REGEX = /^image-set\((?:(?:(?:url|type)\("[^\\"]*"\)|[\d.]+(?:x|dpi|dpcm|dppx)),?\s*)+\)/i;
 
   currentStyle.clear();
   /* eslint-disable-next-line @devtools/no-imperative-dom-api --
@@ -218,9 +241,15 @@
       continue;
     }
 
+    const value = buffer.style.getPropertyValue(property);
+    // We make sure every `image-set()` only uses `url()`s for its images.
+    // If any of them seem malformed, we skip the whole property.
+    const imageSets = [...value.matchAll(IMAGESET_REGEX)];
+    if (imageSets.some(match => !GOOD_IMAGESET_REGEX.test(match[1]))) {
+      continue;
+    }
     // There could be multiple `url()` functions, so we check them all.
     // If any of them is not a `data` URL, we skip the whole property.
-    const value = buffer.style.getPropertyValue(property);
     const potentialUrls = [...value.matchAll(URL_REGEX)].map(match => match[1]);
     if (potentialUrls.some(
             potentialUrl => !Common.ParsedURL.schemeIs(potentialUrl as Platform.DevToolsPath.UrlString, 'data:'))) {
Loading diff…

Original Bug Report

reported by re...@gmail.com

Bypass for console.log %c formatter url filter

VULNERABILITY DETAILS
The mitigations for Issue 40056332 and Issue 40060475 can be bypassed by using specific CSS syntax to load an external url. This leads to minor data exfil in DevTools (ip/ua/headers, whether the console was opened, css media queries etc).

The vulnerable regex is at: https://source.chromium.org/chromium/_/chromium/devtools/devtools-frontend/+/main:front_end/panels/console/ConsoleFormat.ts;l=206;drc=8cf3f6e6e069efefa0ae8ef9557214f62e856f81

VERSION
Chrome Version: 145.0.7587.6 Dev, 145.0.7626.0 Canary
Operating System: Windows, Mac, Linux, ChromeOS

REPRODUCTION CASE
PoC:

console.log("%c\t", `background-image:if(supports(_): "url(data:"; else: url("https://google.com/favicon.ico"));`)

Running the above, the Google favicon will appear in the DevTools console log.

CREDIT INFORMATION
Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited?
Reporter credit: Lyra Rebane (rebane2001)

View on issue tracker