chrome-devtools-frontend
Version:
Chrome DevTools UI
75 lines (67 loc) • 3.4 kB
text/typescript
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as Common from '../../../../core/common/common.js';
import type * as Platform from '../../../../core/platform/platform.js';
/**
* Builds a regex fragment that matches a CSS identifier even when written
* with hex escapes (e.g. `\75\72\6c` for `url`).
*/
export function 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('');
}
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.
// 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;
/**
* Parses `styleToAdd` as a CSS style string and populates `currentStyle`
* with only the properties that pass safety checks:
* - Property name must start with one of the allowed prefixes.
* - `url()` values must use `data:` scheme only.
* - `image-set()` values must use properly formed `url()`s.
*
* The map is cleared before being populated.
*/
export function sanitizeStyle(currentStyle: Map<string, {value: string, priority: string}>, styleToAdd: string): void {
currentStyle.clear();
// eslint-disable-next-line @devtools/no-imperative-dom-api
const buffer = document.createElement('span');
buffer.setAttribute('style', styleToAdd);
for (const property of buffer.style) {
if (!ALLOWED_PROPERTY_PREFIXES.some(prefix =>
property.startsWith(prefix) || property.startsWith(`-webkit-${prefix}`))) {
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 potentialUrls = [...value.matchAll(URL_REGEX)].map(match => match[1]);
if (potentialUrls.some(potentialUrl =>
!Common.ParsedURL.schemeIs(potentialUrl as Platform.DevToolsPath.UrlString, 'data:'))) {
continue;
}
currentStyle.set(property, {
value,
priority: buffer.style.getPropertyPriority(property),
});
}
}