Compare commits

...

2 Commits

Author SHA1 Message Date
Marcel
246568301a refactor(ocr): CSRF-wrap injected fetchImpl too, not just the default
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m19s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m33s
CI / fail2ban Regex (pull_request) Successful in 43s
CI / Semgrep Security Scan (pull_request) Successful in 21s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m2s
CI / Unit & Component Tests (push) Successful in 3m24s
CI / OCR Service Tests (push) Successful in 20s
CI / Backend Unit Tests (push) Successful in 3m32s
CI / fail2ban Regex (push) Successful in 44s
CI / Semgrep Security Scan (push) Successful in 21s
CI / Compose Bucket Idempotency (push) Successful in 1m2s
nightly / deploy-staging (push) Successful in 3m47s
Mirror the useTranscriptionBlocks pattern: makeCsrfFetch(options.fetchImpl
?? fetch) wraps both the default and any injected fetch, so CSRF protection
holds regardless of how the hook is constructed — defense-in-depth against a
future caller injecting a bare fetch. Simplifies the CSRF test to assert on
the injected path instead of stubbing global fetch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 22:10:09 +02:00
Marcel
aab4fe37ae fix(ocr): send CSRF token when starting an OCR run
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 3m16s
CI / OCR Service Tests (pull_request) Successful in 21s
CI / Backend Unit Tests (pull_request) Successful in 3m38s
CI / fail2ban Regex (pull_request) Successful in 42s
CI / Semgrep Security Scan (pull_request) Successful in 19s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m5s
The OCR trigger POST went through bare `fetch`, so it carried no
X-XSRF-TOKEN header. Spring Security rejected it and the UI showed
"Sitzungsfehler. Bitte laden Sie die Seite neu." (CSRF_TOKEN_MISSING).

Default the job controller's fetchImpl to csrfFetch — matching the
autosave hook — so mutating requests are CSRF-protected while GET
polling passes through unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 21:09:18 +02:00
2 changed files with 31 additions and 1 deletions

View File

@@ -139,6 +139,35 @@ describe('createOcrJob.triggerOcr', () => {
});
});
describe('createOcrJob — CSRF on mutating requests', () => {
afterEach(() => {
document.cookie = 'XSRF-TOKEN=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
});
it('injects X-XSRF-TOKEN on the trigger POST', async () => {
document.cookie = 'XSRF-TOKEN=tok-123; path=/';
const fetchImpl = makeFetch({
'/ocr': () =>
new Response(JSON.stringify({ jobId: 'job-1' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
})
});
const job = createOcrJob({ documentId: () => 'doc-1', fetchImpl });
await job.triggerOcr('KURRENT', false);
job.destroy();
const postCall = fetchImpl.mock.calls.find(
([url, init]) =>
url.toString().includes('/ocr') && (init as RequestInit | undefined)?.method === 'POST'
);
expect(postCall).toBeDefined();
const headers = new Headers((postCall![1] as RequestInit).headers);
expect(headers.get('X-XSRF-TOKEN')).toBe('tok-123');
});
});
describe('createOcrJob.checkStatus', () => {
it('starts polling when status is RUNNING with a jobId', async () => {
const fetchImpl = makeFetch({

View File

@@ -1,6 +1,7 @@
import { m } from '$lib/paraglide/messages.js';
import { getErrorMessage } from '$lib/shared/errors';
import { translateOcrProgress } from '$lib/ocr/translateOcrProgress';
import { makeCsrfFetch } from '$lib/shared/cookies';
export interface OcrJobOptions {
documentId: () => string;
@@ -27,7 +28,7 @@ const DEFAULT_RESET_DELAY_MS = 1000;
export function createOcrJob(options: OcrJobOptions): OcrJobController {
const { documentId, onJobFinished } = options;
const fetchImpl = options.fetchImpl ?? fetch;
const fetchImpl = makeCsrfFetch(options.fetchImpl ?? fetch);
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
const resetDelayMs = options.resetDelayMs ?? DEFAULT_RESET_DELAY_MS;