feat(security+obs): SSRF allowlist, HTML sanitization, timing-safe tokens, fetch timeouts, structured logger
Security:
- cms-images / cms-files now share resolveCmsSource (cms-source.server.ts)
which enforces an allowlist: asset filename, absolute path on the CMS
host, or full URL exact-matching the configured CMS host+protocol.
Foreign hosts, protocol-relative URLs and path traversal return 400.
- markdown-safe.ts is the single configured marked entrypoint; it
disables raw HTML rendering globally (renderer.html() returns ''),
removing the inline script/iframe injection vector via markdown.
All 8 marked imports across components and loaders now go through it.
- sanitize-blocks.server.ts walks resolved page/post content and runs
HtmlBlock html fields through sanitize-html with a tight allowlist
(no script/style/on*, only youtube/vimeo/spotify iframes, mailto/tel
plus http(s) schemes, target=_blank gets rel=noopener).
- Webhook tokens (revalidate, cache/clear) compared via
crypto.timingSafeEqual through auth-token.server.ts.
- cms.ts wraps every fetch in cmsFetch(): 5 s default timeout via
AbortSignal.timeout, override per call or globally via
PUBLIC_CMS_FETCH_TIMEOUT_MS. Stops slow CMS responses from pinning
SSR workers indefinitely.
Observability:
- log.server.ts gives logWarn / logError / logInfo with scope + context;
swallowed catch {} blocks in hooks, layout.server, +page.server,
[...slug]/+page.server and sitemap now log instead of going silent.
Routing:
- Legacy /blog, /blog/page/:n, /blog/tag/:t and /p/:slug stub routes
deleted; their 301 redirects moved into hooks.server.ts so the
request never enters the SvelteKit page pipeline.
Tests (vitest, server-side):
- auth-token.server.test.ts (6 cases for constant-time compare)
- cms-source.server.test.ts (8 cases for SSRF allowlist)
- sanitize-blocks.server.test.ts (9 cases for sanitizer + walker)
All 23 green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Zentral konfigurierter `marked`-Parser. Wichtig: deaktiviert das
|
||||
* Rendern von **rohem HTML** in Markdown-Quellen — sonst kann ein
|
||||
* Editor (oder ein kompromittiertes Editor-Konto) `<script>` oder
|
||||
* `<iframe>`-Tags direkt einschleusen. Marked rendert sie standardmäßig
|
||||
* wörtlich durch.
|
||||
*
|
||||
* `marked` ist Singleton: einmaliger Side-Effect-Import setzt die
|
||||
* Renderer-Hooks global. Komponenten und Loader importieren `marked`
|
||||
* von hier statt direkt von "marked".
|
||||
*
|
||||
* Komponenten, die `marked.parse(...)` aufrufen, sollten das Resultat
|
||||
* weiterhin nur über `{@html}` einsetzen — der Sanitizer hier deckt das
|
||||
* Markdown-→-HTML-Mapping ab. Roh-HTML-Felder (HtmlBlock) werden zusätzlich
|
||||
* über `$lib/sanitize-blocks.server` gefiltert.
|
||||
*/
|
||||
import { marked } from 'marked';
|
||||
|
||||
let configured = false;
|
||||
|
||||
function configure() {
|
||||
if (configured) return;
|
||||
marked.setOptions({ gfm: true });
|
||||
marked.use({
|
||||
renderer: {
|
||||
// Inline raw HTML: `<...>` → bleibt wörtlich als Text stehen.
|
||||
html() {
|
||||
return '';
|
||||
},
|
||||
},
|
||||
});
|
||||
configured = true;
|
||||
}
|
||||
|
||||
configure();
|
||||
|
||||
export { marked };
|
||||
Reference in New Issue
Block a user