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/routes/updates | |
| 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/routes/updates')
| -rw-r--r-- | src/routes/updates/+page.svelte | 23 |
1 files changed, 21 insertions, 2 deletions
diff --git a/src/routes/updates/+page.svelte b/src/routes/updates/+page.svelte index 357a5906..0534d0bb 100644 --- a/src/routes/updates/+page.svelte +++ b/src/routes/updates/+page.svelte @@ -5,6 +5,7 @@ import HeadTitle from "$lib/Home/HeadTitle.svelte"; import Skeleton from "$lib/Loading/Skeleton.svelte"; import { createHeightObserver } from "$lib/Utility/html"; import root from "$lib/Utility/root"; +import { sanitizeFeedHtml } from "$lib/Utility/sanitizeHtml"; import locale from "$stores/locale"; let feed: @@ -35,10 +36,28 @@ onMount(async () => { removeHeightObserver = createHeightObserver(false); startTime = performance.now(); - novelFeed = await (await fetch(root("/api/updates/all-novels"))).json(); + + const allNovels = await (await fetch(root("/api/updates/all-novels"))).json(); + + if (allNovels?.data?.items) + for (const item of allNovels.data.items) { + if (item.postfix) item.postfix = sanitizeFeedHtml(item.postfix); + if (item.series) item.series.name = sanitizeFeedHtml(item.series.name); + } + + novelFeed = allNovels; novelEndTime = performance.now() - startTime; startTime = performance.now(); - feed = await (await fetch(root("/api/updates/manga"))).json(); + + const mangaFeed = await (await fetch(root("/api/updates/manga"))).json(); + + if (mangaFeed?.items) + for (const item of mangaFeed.items) { + item.title = sanitizeFeedHtml(item.title); + item.content = sanitizeFeedHtml(item.content); + } + + feed = mangaFeed; mangaEndTime = performance.now() - startTime; }); |