fix: HTML-Beschreibung sanitizen und Key in Status-Schleife ergänzen

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) <noreply@anthropic.com>
This commit is contained in:
Daniel Michelberger 2026-08-06 18:56:03 +02:00
parent a45c5d2908
commit 8e256d0184
3 changed files with 34 additions and 3 deletions

View file

@ -9,6 +9,7 @@
"version": "2.0.0-alpha", "version": "2.0.0-alpha",
"dependencies": { "dependencies": {
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dompurify": "^3.4.13",
"lucide-svelte": "^0.503.0", "lucide-svelte": "^0.503.0",
"maplibre-gl": "^6.2.0", "maplibre-gl": "^6.2.0",
"mode-watcher": "^1.1.0", "mode-watcher": "^1.1.0",
@ -1969,6 +1970,13 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/@vitest/expect": {
"version": "4.1.10", "version": "4.1.10",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz",
@ -2807,6 +2815,15 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/dotenv": {
"version": "16.6.1", "version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",

View file

@ -42,6 +42,7 @@
}, },
"dependencies": { "dependencies": {
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dompurify": "^3.4.13",
"lucide-svelte": "^0.503.0", "lucide-svelte": "^0.503.0",
"maplibre-gl": "^6.2.0", "maplibre-gl": "^6.2.0",
"mode-watcher": "^1.1.0", "mode-watcher": "^1.1.0",

View file

@ -20,6 +20,8 @@
ArrowLeft, Upload, MapPin, MessageSquare, History, ArrowLeft, Upload, MapPin, MessageSquare, History,
Check, Trash2, Download, Plus, AlertTriangle, Check, Trash2, Download, Plus, AlertTriangle,
} from 'lucide-svelte' } from 'lucide-svelte'
import DOMPurify from 'dompurify'
import { browser } from '$app/environment'
import type { TrailCommentsResponse, TrailVersionsResponse } from '$lib/types' import type { TrailCommentsResponse, TrailVersionsResponse } from '$lib/types'
// Generics der JSON-Felder, wie in trails/+page.svelte — die // 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 trail = $derived(page.params.id ? trails.getById(page.params.id) : undefined)
const canEdit = $derived(trail ? trails.canEdit(trail) : false) 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 => { const version = $derived.by((): Version | null => {
if (!trail?.current) return null if (!trail?.current) return null
return (versions.records.find((v) => v.id === trail.current) ?? null) as Version | null return (versions.records.find((v) => v.id === trail.current) ?? null) as Version | null
@ -232,8 +245,8 @@
Trails Trails
</Button> </Button>
<h1 class="text-2xl font-semibold">{trail.name}</h1> <h1 class="text-2xl font-semibold">{trail.name}</h1>
{#if trail.description} {#if safeDescription}
<p class="text-muted-foreground text-sm">{@html trail.description}</p> <p class="text-muted-foreground text-sm">{@html safeDescription}</p>
{/if} {/if}
</div> </div>
@ -252,7 +265,7 @@
{#if canEdit} {#if canEdit}
<div class="flex gap-2"> <div class="flex gap-2">
{#each ['offen', 'eingeschraenkt', 'gesperrt'] as s} {#each ['offen', 'eingeschraenkt', 'gesperrt'] as s (s)}
<Button <Button
variant={trail.status === s ? 'default' : 'outline'} variant={trail.status === s ? 'default' : 'outline'}
size="sm" size="sm"