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
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>
This commit was merged in pull request #705.
This commit is contained in:
@@ -139,29 +139,26 @@ describe('createOcrJob.triggerOcr', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('createOcrJob — CSRF on the default fetch path', () => {
|
describe('createOcrJob — CSRF on mutating requests', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.unstubAllGlobals();
|
|
||||||
document.cookie = 'XSRF-TOKEN=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/';
|
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 () => {
|
it('injects X-XSRF-TOKEN on the trigger POST', async () => {
|
||||||
document.cookie = 'XSRF-TOKEN=tok-123; path=/';
|
document.cookie = 'XSRF-TOKEN=tok-123; path=/';
|
||||||
const globalFetch = vi.fn(
|
const fetchImpl = makeFetch({
|
||||||
async () =>
|
'/ocr': () =>
|
||||||
new Response(JSON.stringify({ jobId: 'job-1' }), {
|
new Response(JSON.stringify({ jobId: 'job-1' }), {
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: { 'Content-Type': 'application/json' }
|
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', fetchImpl });
|
||||||
const job = createOcrJob({ documentId: () => 'doc-1' });
|
|
||||||
await job.triggerOcr('KURRENT', false);
|
await job.triggerOcr('KURRENT', false);
|
||||||
job.destroy();
|
job.destroy();
|
||||||
|
|
||||||
const postCall = globalFetch.mock.calls.find(
|
const postCall = fetchImpl.mock.calls.find(
|
||||||
([url, init]) =>
|
([url, init]) =>
|
||||||
url.toString().includes('/ocr') && (init as RequestInit | undefined)?.method === 'POST'
|
url.toString().includes('/ocr') && (init as RequestInit | undefined)?.method === 'POST'
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { m } from '$lib/paraglide/messages.js';
|
import { m } from '$lib/paraglide/messages.js';
|
||||||
import { getErrorMessage } from '$lib/shared/errors';
|
import { getErrorMessage } from '$lib/shared/errors';
|
||||||
import { translateOcrProgress } from '$lib/ocr/translateOcrProgress';
|
import { translateOcrProgress } from '$lib/ocr/translateOcrProgress';
|
||||||
import { csrfFetch } from '$lib/shared/cookies';
|
import { makeCsrfFetch } from '$lib/shared/cookies';
|
||||||
|
|
||||||
export interface OcrJobOptions {
|
export interface OcrJobOptions {
|
||||||
documentId: () => string;
|
documentId: () => string;
|
||||||
@@ -28,7 +28,7 @@ const DEFAULT_RESET_DELAY_MS = 1000;
|
|||||||
|
|
||||||
export function createOcrJob(options: OcrJobOptions): OcrJobController {
|
export function createOcrJob(options: OcrJobOptions): OcrJobController {
|
||||||
const { documentId, onJobFinished } = options;
|
const { documentId, onJobFinished } = options;
|
||||||
const fetchImpl = options.fetchImpl ?? csrfFetch;
|
const fetchImpl = makeCsrfFetch(options.fetchImpl ?? fetch);
|
||||||
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
||||||
const resetDelayMs = options.resetDelayMs ?? DEFAULT_RESET_DELAY_MS;
|
const resetDelayMs = options.resetDelayMs ?? DEFAULT_RESET_DELAY_MS;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user