From 6a7228c06d7af2a28ead1f4ae1830a258c05afae Mon Sep 17 00:00:00 2001
From: Fuwn <contact@fuwn.net>
Date: Mon, 1 Jun 2026 15:45:01 +0000
Subject: 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.
---
 src/routes/updates/+page.svelte | 23 +++++++++++++++++++++--
 1 file changed, 21 insertions(+), 2 deletions(-)

(limited to 'src/routes/updates/+page.svelte')

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;
 });
 
-- 
cgit v1.2.3

