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:
parent
a45c5d2908
commit
8e256d0184
3 changed files with 34 additions and 3 deletions
17
frontend/package-lock.json
generated
17
frontend/package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue