diff options
Diffstat (limited to 'src/lib/Utility/sanitizeCss.test.ts')
| -rw-r--r-- | src/lib/Utility/sanitizeCss.test.ts | 76 |
1 files changed, 76 insertions, 0 deletions
diff --git a/src/lib/Utility/sanitizeCss.test.ts b/src/lib/Utility/sanitizeCss.test.ts new file mode 100644 index 00000000..f6c22364 --- /dev/null +++ b/src/lib/Utility/sanitizeCss.test.ts @@ -0,0 +1,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"); + }); +}); |