fix(security): P0 — CSRF 방어, h2-console prod 격리, CORS 외부화
- CSRF: CookieCsrfTokenRepository(이중제출 토큰) + SPA용 CsrfTokenRequestAttributeHandler,
CsrfCookieFilter로 XSRF-TOKEN 쿠키 강제 렌더. /api/auth/login·/api/public/** 는 예외.
프론트 api.ts가 변경요청에 X-XSRF-TOKEN 헤더 자동 주입.
- 세션쿠키 SameSite=Lax·HttpOnly, prod는 Secure=${ACS_COOKIE_SECURE:false}(HTTPS 시 활성).
- h2-console permitAll·frameOptions.sameOrigin을 spring.h2.console.enabled에 연동 → prod 자동 비노출.
- CORS allowed-origins를 acs.cors.allowed-origins 프로퍼티로 외부화(prod 기본 빈 값, nginx 동일출처).
- .env.example·docker-compose에 ACS_CORS_ALLOWED_ORIGINS·ACS_COOKIE_SECURE 추가.
검증: 빌드/테스트 통과, curl로 CSRF 차단(403)·토큰 통과(404)·로그인/공개 예외·dev h2-console 확인.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -21,10 +21,22 @@ const BASE = '/api';
|
||||
/** Endpoints whose 401 must NOT trigger a redirect (login probe / public pages). */
|
||||
const NO_REDIRECT_ON_401 = ['/auth/me', '/auth/login', '/auth/logout', '/public/'];
|
||||
|
||||
/**
|
||||
* CSRF double-submit: the backend sets an XSRF-TOKEN cookie; echo it back in the
|
||||
* X-XSRF-TOKEN header on state-changing requests. GET/HEAD need no token.
|
||||
*/
|
||||
function csrfHeaders(method?: string): Record<string, string> {
|
||||
const m = (method ?? 'GET').toUpperCase();
|
||||
if (m === 'GET' || m === 'HEAD') return {};
|
||||
const c = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/);
|
||||
return c ? { 'X-XSRF-TOKEN': decodeURIComponent(c[1]) } : {};
|
||||
}
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const res = await fetch(`${BASE}${path}`, {
|
||||
credentials: 'include',
|
||||
...init,
|
||||
headers: { ...(init?.headers ?? {}), ...csrfHeaders(init?.method) },
|
||||
});
|
||||
// Session expired / not authenticated on a protected call → send to login.
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user