Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in DevTools
DescriptionInsufficient policy enforcement in DevTools
ComponentDevTools
Bug ClassLogic Error
Tracker474670215
Fix commita98136b97706 (devtools/devtools-frontend) +63/-5
CISA KEVNot listed
CreditedLyra Rebane (rebane2001)
Disclosed2026-03-10

Files Changed

  • AUTHORS
  • front_end/panels/console/ConsoleFormat.test.ts
  • front_end/panels/console/ConsoleFormat.ts
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