From 8e256d01846d9bd55fb715a6df350360b6547cb1 Mon Sep 17 00:00:00 2001 From: Daniel Michelberger Date: Thu, 6 Aug 2026 18:56:03 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20HTML-Beschreibung=20sanitizen=20und=20Ke?= =?UTF-8?q?y=20in=20Status-Schleife=20erg=C3=A4nzen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DOMPurify filtert trail.description vor {@html}, da nur Paten/Admins das editor-Feld setzen dürfen, ungefiltert aber Skriptcode bei allen Team-Mitgliedern ausführen könnte. Der Sanitizer läuft browser-gated, da DOMPurify serverseitig kein DOM hat. Zusätzlich Key in der Status-Buttons-Schleife ergänzt. Co-Authored-By: Claude Opus 5 (1M context) --- frontend/package-lock.json | 17 +++++++++++++++++ frontend/package.json | 1 + .../routes/dashboard/trails/[id]/+page.svelte | 19 ++++++++++++++++--- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8f0a693..4cff1f7 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "2.0.0-alpha", "dependencies": { "date-fns": "^4.1.0", + "dompurify": "^3.4.13", "lucide-svelte": "^0.503.0", "maplibre-gl": "^6.2.0", "mode-watcher": "^1.1.0", @@ -1969,6 +1970,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@vitest/expect": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", @@ -2807,6 +2815,15 @@ "dev": true, "license": "MIT" }, + "node_modules/dompurify": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz", + "integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 87b66b4..e2f5ea5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -42,6 +42,7 @@ }, "dependencies": { "date-fns": "^4.1.0", + "dompurify": "^3.4.13", "lucide-svelte": "^0.503.0", "maplibre-gl": "^6.2.0", "mode-watcher": "^1.1.0", diff --git a/frontend/src/routes/dashboard/trails/[id]/+page.svelte b/frontend/src/routes/dashboard/trails/[id]/+page.svelte index 95b0adf..abd3631 100644 --- a/frontend/src/routes/dashboard/trails/[id]/+page.svelte +++ b/frontend/src/routes/dashboard/trails/[id]/+page.svelte @@ -20,6 +20,8 @@ ArrowLeft, Upload, MapPin, MessageSquare, History, Check, Trash2, Download, Plus, AlertTriangle, } from 'lucide-svelte' + import DOMPurify from 'dompurify' + import { browser } from '$app/environment' import type { TrailCommentsResponse, TrailVersionsResponse } from '$lib/types' // Generics der JSON-Felder, wie in trails/+page.svelte — die @@ -40,6 +42,17 @@ const trail = $derived(page.params.id ? trails.getById(page.params.id) : undefined) const canEdit = $derived(trail ? trails.canEdit(trail) : false) + /** + * description ist ein editor-Feld und enthält HTML. Gesetzt wird es zwar + * nur von Paten und Admins, aber ungefiltert ausgegeben wäre es ein Weg, + * Skriptcode bei allen Team-Mitgliedern auszuführen. DOMPurify braucht ein + * DOM — beim serverseitigen Rendern (kein `window`/`document`) liefern + * wir stattdessen leer; der Browser holt das beim Hydrieren nach. + */ + const safeDescription = $derived( + browser && trail?.description ? DOMPurify.sanitize(trail.description) : '', + ) + const version = $derived.by((): Version | null => { if (!trail?.current) return null return (versions.records.find((v) => v.id === trail.current) ?? null) as Version | null @@ -232,8 +245,8 @@ Trails

{trail.name}

- {#if trail.description} -

{@html trail.description}

+ {#if safeDescription} +

{@html safeDescription}

{/if} @@ -252,7 +265,7 @@ {#if canEdit}
- {#each ['offen', 'eingeschraenkt', 'gesperrt'] as s} + {#each ['offen', 'eingeschraenkt', 'gesperrt'] as s (s)}