aboutsummaryrefslogtreecommitdiff
path: root/src/lib/Utility/sanitizeCss.ts
blob: 976fa6a1523baca0e803495aa862c3c51be14a50 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import * as csstree from "css-tree";

const blockedProperties = new Set(["behavior", "-moz-binding"]);
const dangerousValue = /expression\s*\(|javascript:|vbscript:/i;

/**
 * Sanitise user-supplied badge-wall CSS at the trust boundary (write time), so
 * the stored value is safe regardless of how it is later rendered. Parsing with
 * css-tree (leniently, like a browser) drops anything that isn't valid CSS —
 * including `</style>` break-out attempts — and we additionally remove the few
 * constructs that can load resources or (in legacy engines) run code:
 * `@import`, `behavior`/`-moz-binding`, and `expression()`/`javascript:` values.
 *
 * This is defence in depth: rendering goes through `textContent` (no HTML
 * parsing, so no break-out) and the CSP blocks inline script regardless.
 */
export const sanitizeBadgeWallCss = (css: string): string => {
	if (!css) return "";

	let ast: csstree.CssNode;

	try {
		ast = csstree.parse(css, { onParseError: () => {} });
	} catch {
		return "";
	}

	csstree.walk(ast, (node, item, list) => {
		if (!list || !item) return;

		if (node.type === "Atrule" && node.name.toLowerCase() === "import") {
			list.remove(item);
		} else if (
			node.type === "Rule" &&
			csstree.generate(node.prelude).includes("<")
		) {
			// css-tree keeps an unparseable selector as a Raw prelude, so a
			// `</style><script>…` break-out shows up as a rule whose selector
			// contains `<` (never valid in a real selector). Drop the rule.
			list.remove(item);
		} else if (node.type === "Raw" && /[<>]/.test(node.value)) {
			list.remove(item);
		} else if (
			node.type === "Declaration" &&
			(blockedProperties.has(node.property.toLowerCase()) ||
				dangerousValue.test(csstree.generate(node.value)))
		) {
			list.remove(item);
		}
	});

	try {
		return csstree.generate(ast);
	} catch {
		return "";
	}
};