fix(csrf): send X-XSRF-TOKEN on all client-side mutating fetch calls
Some checks failed
CI / Unit & Component Tests (push) Has been cancelled
CI / OCR Service Tests (push) Has been cancelled
CI / Backend Unit Tests (push) Has been cancelled
CI / fail2ban Regex (push) Has been cancelled
CI / Semgrep Security Scan (push) Has been cancelled
CI / Compose Bucket Idempotency (push) Has been cancelled
CI / Unit & Component Tests (pull_request) Successful in 3m34s
CI / OCR Service Tests (pull_request) Successful in 20s
CI / fail2ban Regex (pull_request) Has been cancelled
CI / Semgrep Security Scan (pull_request) Has been cancelled
CI / Compose Bucket Idempotency (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled

hooks.server.ts already forwards the CSRF token for server-side fetch
(form actions, load). Client-side XHR calls bypassed it, causing Spring
Security to return 403 before PermissionAspect even ran.

Adds getCsrfToken/withCsrf/makeCsrfFetch to cookies.ts.
useTranscriptionBlocks wraps its injectable fetchImpl with makeCsrfFetch
(covers all block mutations and saveBlockWithConflictRetry).
useBlockAutoSave, TranscriptionEditView, BulkDocumentEditLayout,
OcrTrainingCard, and SegmentationTrainingCard apply withCsrf inline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit was merged in pull request #648.
This commit is contained in:
Marcel
2026-05-20 20:10:12 +02:00
committed by marcel
parent 909f960b2e
commit 19e2f65a21
7 changed files with 73 additions and 15 deletions

View File

@@ -1,5 +1,6 @@
import { SvelteMap } from 'svelte/reactivity';
import type { PersonMention } from '$lib/shared/types';
import { withCsrf } from '$lib/shared/cookies';
export type SaveState = 'idle' | 'saving' | 'saved' | 'fading' | 'error';
@@ -116,12 +117,15 @@ export function createBlockAutoSave({ saveFn, documentId }: Options) {
for (const [blockId, text] of pendingTexts) {
const mentions = pendingMentions.get(blockId) ?? [];
clearDebounce(blockId);
void fetch(`/api/documents/${documentId}/transcription-blocks/${blockId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, mentionedPersons: mentions }),
keepalive: true
});
void fetch(
`/api/documents/${documentId}/transcription-blocks/${blockId}`,
withCsrf({
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text, mentionedPersons: mentions }),
keepalive: true
})
);
pendingTexts.delete(blockId);
pendingMentions.delete(blockId);
}