CVE-2026-3941
Overview
Files Changed
AUTHORSfront_end/panels/console/ConsoleFormat.test.tsfront_end/panels/console/ConsoleFormat.ts
Patch
From a98136b97706f7972cbc99b69e283398d90a442c Mon Sep 17 00:00:00 2001 From: Lyra Rebane <[email protected]> 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 <[email protected]> Reviewed-by: Simon Zünd <[email protected]> Reviewed-by: Philip Pfaffe <[email protected]> --- diff --git a/AUTHORS b/AUTHORS index ff02b4a..5a301e4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -64,6 +64,7 @@ Liam DeBeasi <[email protected]> Luke Swiderski <[email protected]> Luke Warlow <[email protected]> +Lyra Rebane <[email protected]> Marijn Haverbeke <[email protected]> Max 😎 Coplan <[email protected]> Michael Brüning <[email protected]> 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:'))) {
Original Bug Report
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)