fix(security): add csrfFetch wrapper and apply to all client-side mutating requests
Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 2m52s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m48s
CI / fail2ban Regex (pull_request) Successful in 44s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m4s

Introduces `csrfFetch` (= `makeCsrfFetch(fetch)`) in cookies.ts as a
drop-in fetch replacement that auto-injects X-XSRF-TOKEN on POST/PUT/PATCH/DELETE.

Previously 8 call sites sent mutating requests without the CSRF header —
annotation resize, comment POST/PATCH/DELETE, Geschichte CRUD, Stammbaum
relationship creation, bulk-edit PATCH, and file upload — all would fail
with CSRF_TOKEN_MISSING if the backend's cookie-based protection triggered.

All 14 client-side mutating fetches now use csrfFetch; withCsrf/makeCsrfFetch
remain in the API for injectable-fetch use cases (e.g. useTranscriptionBlocks).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-30 10:50:56 +02:00
parent 8cc6031ef0
commit 58254b492b
15 changed files with 53 additions and 44 deletions

View File

@@ -17,7 +17,7 @@ import PdfViewer from '$lib/document/viewer/PdfViewer.svelte';
import { bulkTitleFromFilename } from '$lib/document/filename';
import type { Tag } from '$lib/tag/TagInput.svelte';
import type { components } from '$lib/generated/api';
import { withCsrf } from '$lib/shared/cookies';
import { csrfFetch } from '$lib/shared/cookies';
type Person = components['schemas']['Person'];
@@ -184,10 +184,10 @@ async function saveUpload() {
// FormData with per-chunk progress. Session cookie is sent automatically
// by the browser for same-origin requests.
try {
const res = await fetch(
'/api/documents/quick-upload',
withCsrf({ method: 'POST', body: formData })
);
const res = await csrfFetch('/api/documents/quick-upload', {
method: 'POST',
body: formData
});
const body = await res.json().catch(() => ({ errors: [] }));
const errorFilenames = new Set<string>(
(body.errors ?? []).map((err: { filename: string }) => err.filename)
@@ -241,7 +241,7 @@ async function saveBulkEdit() {
for (let i = 0; i < chunks.length; i++) {
const chunk = chunks[i];
try {
const res = await fetch('/api/documents/bulk', {
const res = await csrfFetch('/api/documents/bulk', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...dto, documentIds: chunk })