diff options
| author | Fuwn <[email protected]> | 2026-06-01 15:45:01 +0000 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-06-01 15:45:01 +0000 |
| commit | 6a7228c06d7af2a28ead1f4ae1830a258c05afae (patch) | |
| tree | 26a1fc3cc8546bd15dac92910998afb8c2a67fd9 /src/lib/Utility/sanitizeHtml.test.ts | |
| parent | fix(security): allow-list web-push endpoints to stop SSRF (diff) | |
| download | due.moe-6a7228c06d7af2a28ead1f4ae1830a258c05afae.tar.xz due.moe-6a7228c06d7af2a28ead1f4ae1830a258c05afae.zip | |
fix(security): sanitize third-party RSS HTML before {@html}
The /updates page rendered manga/novel feed fields (content, titles,
series names) from mangaupdates/syosetu/wlnupdates via {@html} with no
sanitization. CSP already blocks script execution, but injected markup
could still phish, redirect, or track. Add sanitizeFeedHtml (DOMPurify
with a small safe allow-list) and apply it on ingest. A behaviour-gate
test plus a check against the live mangaupdates feed confirm legitimate
formatting (entities, <i>/<b>/<a href>) is preserved while <script>,
event handlers, <iframe>/<meta>/<style> and javascript: URLs are removed.
Diffstat (limited to 'src/lib/Utility/sanitizeHtml.test.ts')
| -rw-r--r-- | src/lib/Utility/sanitizeHtml.test.ts | 57 |
1 files changed, 57 insertions, 0 deletions
diff --git a/src/lib/Utility/sanitizeHtml.test.ts b/src/lib/Utility/sanitizeHtml.test.ts new file mode 100644 index 00000000..1094635e --- /dev/null +++ b/src/lib/Utility/sanitizeHtml.test.ts @@ -0,0 +1,57 @@ +// @vitest-environment jsdom +import { describe, expect, it } from "vitest"; +import { sanitizeFeedHtml } from "./sanitizeHtml"; + +describe("sanitizeFeedHtml", () => { + // Behaviour gate: the formatting real feeds use must survive untouched. + it("preserves entities, inline formatting and safe links", () => { + expect(sanitizeFeedHtml("Fruits & Vegetables")).toBe( + "Fruits & Vegetables", + ); + expect(sanitizeFeedHtml("<i>italic</i> and <b>bold</b>")).toBe( + "<i>italic</i> and <b>bold</b>", + ); + expect(sanitizeFeedHtml("Vol. 1 <em>Ch.</em> 5")).toBe( + "Vol. 1 <em>Ch.</em> 5", + ); + expect( + sanitizeFeedHtml('<a href="https://example.com/x">link</a>'), + ).toContain('href="https://example.com/x"'); + expect(sanitizeFeedHtml("line<br>break")).toContain("<br"); + }); + + it("returns empty string for nullish input", () => { + expect(sanitizeFeedHtml(undefined)).toBe(""); + expect(sanitizeFeedHtml(null)).toBe(""); + expect(sanitizeFeedHtml("")).toBe(""); + }); + + // The fix: scripts, handlers, dangerous tags and URLs must be removed. + it("strips scripts, event handlers and dangerous tags/urls", () => { + const script = sanitizeFeedHtml("<script>alert(1)</script>safe"); + expect(script).not.toContain("script"); + expect(script).toContain("safe"); + + const onerror = sanitizeFeedHtml("before<img src=x onerror=alert(1)>after"); + expect(onerror).not.toContain("onerror"); + expect(onerror).not.toContain("<img"); + expect(onerror).toContain("before"); + expect(onerror).toContain("after"); + + expect( + sanitizeFeedHtml('<a href="javascript:alert(1)">x</a>'), + ).not.toContain("javascript:"); + expect( + sanitizeFeedHtml('<iframe src="https://evil.example.com"></iframe>'), + ).not.toContain("iframe"); + expect( + sanitizeFeedHtml( + '<meta http-equiv="refresh" content="0;url=https://evil.example.com">', + ), + ).not.toContain("meta"); + expect(sanitizeFeedHtml("<style>body{display:none}</style>")).not.toContain( + "style", + ); + expect(sanitizeFeedHtml('<div onclick="steal()">text</div>')).toBe("text"); + }); +}); |