feat(security): add CSRF protection for all state-changing API routes
Build & Push Docker Image / build (push) Successful in 2m16s

- Synchronizer token stored in session; GET /auth/csrf to obtain it
- csrfProtection middleware requires X-CSRF-Token on non-safe methods
- customFetch injects the header via setCsrfTokenGetter
- toolrate boot loads token; reload after local login (session regenerate)
- OpenAPI GET /auth/csrf + CsrfToken schema, orval regenerated
This commit is contained in:
opencode
2026-08-03 10:30:13 +02:00
parent f851305d78
commit bcae59626f
15 changed files with 209 additions and 1 deletions
+18
View File
@@ -17,6 +17,7 @@ const DEFAULT_JSON_ACCEPT = "application/json, application/problem+json";
let _baseUrl: string | null = null;
let _authTokenGetter: AuthTokenGetter | null = null;
let _csrfTokenGetter: (() => string | null) | null = null;
/**
* Set a base URL that is prepended to every relative request URL
@@ -44,6 +45,15 @@ export function setAuthTokenGetter(getter: AuthTokenGetter | null): void {
_authTokenGetter = getter;
}
/**
* Register a getter that supplies a CSRF token. Before every state-changing
* fetch an `X-CSRF-Token` header is attached when the getter returns a value.
* Pass `null` to clear the getter.
*/
export function setCsrfTokenGetter(getter: (() => string | null) | null): void {
_csrfTokenGetter = getter;
}
function isRequest(input: RequestInfo | URL): input is Request {
return typeof Request !== "undefined" && input instanceof Request;
}
@@ -349,6 +359,14 @@ export async function customFetch<T = unknown>(
headers.set("accept", DEFAULT_JSON_ACCEPT);
}
// Attach CSRF token for state-changing requests, unless one is already set.
if (_csrfTokenGetter && !headers.has("x-csrf-token")) {
const csrf = _csrfTokenGetter();
if (csrf) {
headers.set("x-csrf-token", csrf);
}
}
// Attach bearer token when an auth getter is configured and no
// Authorization header has been explicitly provided.
if (_authTokenGetter && !headers.has("authorization")) {