fix(csrf): send X-XSRF-TOKEN on all client-side mutating fetch calls
Some checks failed
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
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
Some checks failed
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
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
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:
@@ -6,6 +6,7 @@ import TranscribeCoachEmptyState from '$lib/shared/help/TranscribeCoachEmptyStat
|
||||
import type { PersonMention, TranscriptionBlockData } from '$lib/shared/types';
|
||||
import { createBlockAutoSave } from '$lib/document/transcription/useBlockAutoSave.svelte';
|
||||
import { createBlockDragDrop } from '$lib/document/transcription/useBlockDragDrop.svelte';
|
||||
import { withCsrf } from '$lib/shared/cookies';
|
||||
|
||||
type Props = {
|
||||
documentId: string;
|
||||
@@ -113,11 +114,14 @@ function handleDelete(blockId: string) {
|
||||
|
||||
async function reorder(newOrder: string[]) {
|
||||
try {
|
||||
const res = await fetch(`/api/documents/${documentId}/transcription-blocks/reorder`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ blockIds: newOrder })
|
||||
});
|
||||
const res = await fetch(
|
||||
`/api/documents/${documentId}/transcription-blocks/reorder`,
|
||||
withCsrf({
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ blockIds: newOrder })
|
||||
})
|
||||
);
|
||||
if (!res.ok) return;
|
||||
const updated = await res.json();
|
||||
for (const b of updated) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
lastEditedAt's $derived are scope-local to one computation; they're never
|
||||
stored on $state. */
|
||||
import type { TranscriptionBlockData, PersonMention } from '$lib/shared/types';
|
||||
import { makeCsrfFetch } from '$lib/shared/cookies';
|
||||
import { saveBlockWithConflictRetry } from './saveBlockWithConflictRetry';
|
||||
import { BlockConflictResolvedError } from './blockConflictMerge';
|
||||
|
||||
@@ -41,7 +42,7 @@ export function createTranscriptionBlocks(
|
||||
options: TranscriptionBlocksOptions
|
||||
): TranscriptionBlocksController {
|
||||
const { documentId } = options;
|
||||
const fetchImpl = options.fetchImpl ?? fetch;
|
||||
const fetchImpl = makeCsrfFetch(options.fetchImpl ?? fetch);
|
||||
|
||||
let blocks = $state<TranscriptionBlockData[]>([]);
|
||||
let annotationReloadKey = $state(0);
|
||||
|
||||
Reference in New Issue
Block a user