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 "";
}
};
|