a2ca0db9ca
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>
45 lines
1.5 KiB
TypeScript
45 lines
1.5 KiB
TypeScript
/**
|
|
* Minimaler strukturierter Logger für Server-Code. Stdout/stderr in einem
|
|
* konsistenten Format, sodass Caddy/Docker-Aggregation davor zurechtkommt.
|
|
*
|
|
* Bewusst keine externe Lib (pino, winston) — Adapter-Node läuft lean,
|
|
* und das kleine Volumen rechtfertigt keinen extra Dep + Bundle-Overhead.
|
|
*
|
|
* Nutzung in `catch`-Blöcken statt swallowen:
|
|
*
|
|
* } catch (err) {
|
|
* logWarn('cms.batchFetch', err, { collection });
|
|
* }
|
|
*
|
|
* Der Scope (`'cms.batchFetch'`) ist freier Text — Konvention: `area.action`.
|
|
*/
|
|
|
|
type LogContext = Record<string, unknown>;
|
|
|
|
function format(level: string, scope: string, err: unknown, ctx?: LogContext): string {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
const stack = err instanceof Error && err.stack ? err.stack : undefined;
|
|
const parts: string[] = [`[${level}] ${scope}: ${message}`];
|
|
if (ctx && Object.keys(ctx).length) {
|
|
parts.push(JSON.stringify(ctx));
|
|
}
|
|
if (stack && level === 'error') {
|
|
parts.push(stack);
|
|
}
|
|
return parts.join(' ');
|
|
}
|
|
|
|
export function logWarn(scope: string, err: unknown, ctx?: LogContext): void {
|
|
console.warn(format('warn', scope, err, ctx));
|
|
}
|
|
|
|
export function logError(scope: string, err: unknown, ctx?: LogContext): void {
|
|
console.error(format('error', scope, err, ctx));
|
|
}
|
|
|
|
export function logInfo(scope: string, message: string, ctx?: LogContext): void {
|
|
const parts: string[] = [`[info] ${scope}: ${message}`];
|
|
if (ctx && Object.keys(ctx).length) parts.push(JSON.stringify(ctx));
|
|
console.log(parts.join(' '));
|
|
}
|