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
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>
This commit is contained in:
@@ -139,6 +139,38 @@ describe('createOcrJob.triggerOcr', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('createOcrJob — CSRF on the default fetch path', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
document.cookie = 'XSRF-TOKEN=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
|
||||
});
|
||||
|
||||
it('injects X-XSRF-TOKEN on the trigger POST when no fetchImpl is provided', async () => {
|
||||
document.cookie = 'XSRF-TOKEN=tok-123; path=/';
|
||||
const globalFetch = vi.fn(
|
||||
async () =>
|
||||
new Response(JSON.stringify({ jobId: 'job-1' }), {
|
||||
status: 200,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
})
|
||||
);
|
||||
vi.stubGlobal('fetch', globalFetch);
|
||||
|
||||
// No fetchImpl — exercises the production default path used by the document page.
|
||||
const job = createOcrJob({ documentId: () => 'doc-1' });
|
||||
await job.triggerOcr('KURRENT', false);
|
||||
job.destroy();
|
||||
|
||||
const postCall = globalFetch.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({
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { m } from '$lib/paraglide/messages.js';
|
||||
import { getErrorMessage } from '$lib/shared/errors';
|
||||
import { translateOcrProgress } from '$lib/ocr/translateOcrProgress';
|
||||
import { csrfFetch } 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 = options.fetchImpl ?? csrfFetch;
|
||||
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
||||
const resetDelayMs = options.resetDelayMs ?? DEFAULT_RESET_DELAY_MS;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user