aboutsummaryrefslogtreecommitdiff
path: root/src/lib/Utility/sanitizeCss.test.ts
blob: f6c22364a741fc93ecd5bb21c058d75ba193726d (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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import { describe, expect, it } from "vitest";
import { sanitizeBadgeWallCss } from "./sanitizeCss";

describe("sanitizeBadgeWallCss", () => {
	// Behaviour gate: the CSS people actually write for their badge wall survives.
	it("preserves ordinary rules, declarations and values", () => {
		const out = sanitizeBadgeWallCss(
			".badge { color: red; opacity: 0.5; border-radius: 8px; }",
		);

		expect(out).toContain("color");
		expect(out).toContain("red");
		expect(out).toContain("border-radius");
	});

	it("preserves backdrop-filter, content, url() and at-rules", () => {
		expect(
			sanitizeBadgeWallCss(".x { backdrop-filter: blur(4px); }"),
		).toContain("backdrop-filter");
		expect(sanitizeBadgeWallCss('.x::before { content: "★"; }')).toContain(
			"content",
		);
		expect(
			sanitizeBadgeWallCss(
				".x { background: url(https://cdn.due.moe/a.png); }",
			),
		).toContain("url(https://cdn.due.moe/a.png)");
		expect(
			sanitizeBadgeWallCss("@media (min-width: 1px) { .x { color: blue; } }"),
		).toContain("@media");
		expect(
			sanitizeBadgeWallCss(
				"@keyframes spin { from { transform: rotate(0); } to { transform: rotate(360deg); } }",
			),
		).toContain("@keyframes");
	});

	it("returns empty string for nullish input", () => {
		expect(sanitizeBadgeWallCss("")).toBe("");
		// @ts-expect-error exercising defensive nullish handling
		expect(sanitizeBadgeWallCss(undefined)).toBe("");
	});

	// The fix: dangerous constructs are removed while surrounding CSS is kept.
	it("strips @import, behavior, -moz-binding, expression and js: urls", () => {
		const imported = sanitizeBadgeWallCss(
			"@import url(https://evil.example.com/x.css); .x { color: red; }",
		);
		expect(imported).not.toContain("@import");
		expect(imported).toContain("color");

		expect(
			sanitizeBadgeWallCss(".x { behavior: url(evil.htc); color: red; }"),
		).not.toContain("behavior");
		expect(
			sanitizeBadgeWallCss(".x { -moz-binding: url(evil.xml#x); }"),
		).not.toContain("-moz-binding");
		expect(
			sanitizeBadgeWallCss(".x { width: expression(alert(1)); }"),
		).not.toContain("expression");
		expect(
			sanitizeBadgeWallCss(".x { background: url(javascript:alert(1)); }"),
		).not.toContain("javascript:");
	});

	it("drops <style> break-out attempts entirely", () => {
		const out = sanitizeBadgeWallCss(
			".a { color: red; } </style><script>window.x=1</script> .b { color: blue; }",
		);

		expect(out).not.toContain("<script");
		expect(out).not.toContain("</style");
		expect(out).not.toContain("window.x");
		expect(out).toContain("color");
	});
});