feat: 쿠키 인증 API CSRF 경계 도입 - #651
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2af5bb0398
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| @router.get("/csrf", response_model=CsrfTokenResponse) | ||
| def bootstrap_csrf_token( | ||
| request: Request, | ||
| response: Response, | ||
| db: Session = Depends(get_db), |
There was a problem hiding this comment.
CSRF bootstrap의 cross-site 쿠키 덮어쓰기를 차단하세요
사용자가 공격자 페이지나 침해된 same-site sibling을 방문한 상태에서는 <img> 같은 cross-origin GET으로 이 엔드포인트를 호출할 수 있고, 응답 본문을 읽지 못하더라도 새 csrf_token Set-Cookie는 적용됩니다. 그러면 클라이언트 메모리에 캐시된 헤더 토큰과 쿠키가 달라져 다음 workflow 저장·로그인 같은 POST/PATCH가 403으로 실패하며, 해당 메서드는 의도적으로 자동 재시도되지 않으므로 반복 호출로 핵심 흐름을 계속 막을 수 있습니다. Bootstrap에서도 Fetch Metadata/허용 Origin을 검사하거나 신뢰되지 않은 요청이 기존 쿠키를 회전시키지 못하게 해야 합니다.
AGENTS.md reference: AGENTS.md:L193-L194
Useful? React with 👍 / 👎.
| fetch(`${apiBaseUrl}/auth/csrf`, { | ||
| method: 'GET', | ||
| headers, | ||
| credentials: 'include', | ||
| cache: 'no-store', |
There was a problem hiding this comment.
CSRF bootstrap과 실제 요청의 origin을 일치시키세요
NEXT_PUBLIC_API_URL을 https://api.example.com처럼 별도 공개 API origin으로 설정하면 token bootstrap은 항상 그 origin으로 전송되어 host-only csrf_token도 API 호스트에 설정되지만, workflowApi, webhookApi, Settings와 Wizard의 보호 요청은 계속 상대 /api/v1로 웹 호스트에 전송됩니다. 따라서 헤더 token은 첨부되어도 해당 호스트에는 CSRF cookie가 없어 모든 workflow 생성·저장·배포 등의 mutation이 403으로 실패합니다. Bootstrap을 실제 대상 요청 origin별로 수행하거나 모든 보호 consumer가 동일한 API base URL을 사용하도록 맞춰야 합니다.
AGENTS.md reference: apps/client/AGENTS.md:L7-L10
Useful? React with 👍 / 👎.
| and boundary is not None | ||
| and _MULTIPART_BOUNDARY_PATTERN.fullmatch(boundary) is not None | ||
| ) | ||
| return False |
There was a problem hiding this comment.
빈 Axios POST의 content type을 bodyless route에서 허용하세요
Webhook capture 취소는 api.post(..., null, ...)을 사용하며 Axios 1.13은 이 빈 POST에 Content-Type: application/x-www-form-urlencoded를 설정합니다. Route inventory는 이 endpoint를 BODY_OPTIONAL로 분류하지만, 현재 검사는 Content-Type이 존재하면 JSON 또는 multipart만 허용하므로 유효한 CSRF token을 첨부해도 취소 요청이 항상 403으로 종료됩니다. Zero-length BODY_OPTIONAL 요청을 media type 검사 전에 허용하거나 해당 client가 JSON/undefined body를 보내도록 계약을 맞춰야 합니다.
AGENTS.md reference: apps/AGENTS.md:L7-L10
Useful? React with 👍 / 👎.
| except HTTPException as exc: | ||
| if exc.status_code != 401: | ||
| raise |
There was a problem hiding this comment.
AuthService.get_user_from_token은 비활성화된 사용자에 대해 403을 반환하지만 여기서는 401만 invalid session으로 처리하고 403을 그대로 다시 던집니다. 따라서 계정이 비활성화된 뒤 기존 쿠키를 가진 브라우저는 CSRF bootstrap 단계에서 계속 실패하고, logout 요청조차 token을 얻지 못해 인증 쿠키를 지울 수 없으며 Client의 raw bootstrap fetch도 이 403을 로그인 리다이렉트나 anonymous 복구로 처리하지 않습니다. 비활성 상태에서도 실행은 fail-closed로 유지하되 stale auth/CSRF cookie를 삭제해 세션 lifecycle과 logout 경로를 복구해야 합니다.
AGENTS.md reference: AGENTS.md:L140-L144
Useful? React with 👍 / 👎.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 04e4fe1d43
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const error = rawError as AxiosError; | ||
| if (!isFixedCsrfFailure(error)) return Promise.reject(rawError); | ||
|
|
||
| invalidateCsrfToken(); |
There was a problem hiding this comment.
만료된 토큰을 사용한 여러 PUT/DELETE 요청이 비슷한 시점에 403을 받으면, 첫 요청이 재시도용 bootstrap을 시작한 뒤 두 번째 응답의 무조건적인 invalidateCsrfToken()이 generation을 다시 올릴 수 있습니다. 그러면 첫 bootstrap은 CSRF token bootstrap was invalidated로 실패하여 replay-safe 요청 중 하나가 복구되지 않습니다. 실패한 토큰이 아직 현재 cache일 때만 무효화하거나 동시 403들이 하나의 refresh generation을 공유하도록 해야 합니다.
AGENTS.md reference: apps/client/AGENTS.md:L11-L11
Useful? React with 👍 / 👎.
| def _safe_request_id(value: object) -> str: | ||
| if ( | ||
| isinstance(value, str) | ||
| and 1 <= len(value) <= 128 | ||
| and all(ord(character) >= 32 for character in value) | ||
| ): | ||
| return value |
There was a problem hiding this comment.
감사용 request ID를 제한된 형식으로 검증하세요
CSRF 거부 요청의 X-Request-ID에 유효한 CSRF token이나 PII를 넣으면, 현재 검사는 길이 128 이하의 printable 문자열을 그대로 승인하고 이후 record_csrf_denial이 그 값을 audit metadata에 영구 저장합니다. 실제 v1 token도 이 길이 안에 들어오므로 이 변경이 보장하는 token 미기록 경계를 우회할 수 있습니다. UUID 등 제한된 request-ID 형식만 허용하고 나머지는 서버 생성 값으로 교체해야 합니다.
AGENTS.md reference: apps/AGENTS.md:L14-L14
Useful? React with 👍 / 👎.
|
|
||
| | 필드 | 타입 | 필수 | 비고 | | ||
| | --- | --- | --- | --- | | ||
| | GET | `/auth/csrf` | Cookie-authenticated/pre-auth mutation용 10분 signed CSRF token과 host-only HttpOnly cookie를 발급한다. | Safe bootstrap; resource permission 없음 | |
There was a problem hiding this comment.
이 행은 엔드포인트 목록이 아니라 signup 요청 본문 표 안에 들어가 있어 GET이 필드명이고 /auth/csrf가 타입인 것처럼 잘못된 API 계약을 문서화합니다. 같은 행이 login 요청 필드 표와 Google OAuth next query 표에도 반복되어 있으므로 세 곳에서 제거해야 합니다.
AGENTS.md reference: docs/AGENTS.md:L47-L49
Useful? React with 👍 / 👎.
| or len(cookie_token) > _MAX_TOKEN_LENGTH | ||
| ): | ||
| return CsrfValidationReason.TOKEN_INVALID | ||
| if not hmac.compare_digest(header_token, cookie_token): |
There was a problem hiding this comment.
비 ASCII CSRF 토큰을 고정 403으로 거부하세요
클라이언트가 header와 cookie에 같은 비 ASCII 문자열(예: é)을 보내면 Python의 hmac.compare_digest(str, str)가 TypeError: comparing strings with non-ASCII characters is not supported를 발생시킵니다. 이 예외는 여기서 처리되지 않아 고정 auth.csrf_validation_failed 응답 대신 500이 반환되므로, 비교 전에 토큰 문자 집합을 검증하거나 ASCII bytes로 안전하게 변환한 뒤 비교해야 합니다.
AGENTS.md reference: apps/AGENTS.md:L8-L8
Useful? React with 👍 / 👎.
| callback_result = self._on_denied( | ||
| reason, | ||
| policy, | ||
| request.method.upper(), | ||
| request_id, | ||
| ) | ||
| if inspect.isawaitable(callback_result): | ||
| await callback_result |
There was a problem hiding this comment.
CSRF 거부 audit 저장을 이벤트 루프 밖으로 옮기세요
인터넷에서 인증 없이 호출 가능한 login/signup을 포함해 CSRF 검증에 실패할 때마다 이 async middleware가 동기 callback을 직접 실행하고, 실제 record_csrf_denial은 record_audit를 통해 새 SQLAlchemy session을 열어 PostgreSQL commit이 끝날 때까지 기다립니다. 공격자가 잘못된 Origin이나 누락 token 요청을 반복하면 각 요청의 DB I/O가 Gateway 이벤트 루프를 막아 정상 요청 전체의 처리도 지연될 수 있으므로, callback을 thread pool이나 비동기·bounded 관측 경로로 넘겨야 합니다.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 477d9ce4b5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const res = await csrfFetch('/api/v1/code-wizard/generate', { | ||
| method: 'POST', | ||
| headers: { 'Content-Type': 'application/json' }, |
There was a problem hiding this comment.
위저드의 CSRF scope를 실제 workflow 조직에 맞추세요
organizationId prop이 localStorage의 active organization과 다른 경우(이 상태는 새 테스트에서도 명시적으로 다룹니다), 이 호출은 body에는 workflow 조직을 넣지만 X-Organization-Id를 생략하므로 csrfFetch가 stale localStorage 조직으로 bootstrap과 mutation header를 채웁니다. 그 결과 조직 B의 credential로 외부 LLM을 호출하면서 CSRF token은 조직 A에 결박되어 ADR의 organization replay 경계가 실제 소비 조직을 보호하지 못합니다. Code/Prompt/Template 위저드 모두 resolvedOrganizationId를 X-Organization-Id에도 명시해 body, token scope와 Gateway 요청 문맥을 일치시켜야 합니다.
AGENTS.md reference: apps/client/AGENTS.md:L7-L10
Useful? React with 👍 / 👎.
| except ValueError: | ||
| raise HTTPException( | ||
| status_code=400, | ||
| detail="Invalid CSRF request context", | ||
| ) from None |
There was a problem hiding this comment.
X-Organization-Id가 128자를 넘거나 제어 문자를 포함하면 _normalize_scope가 ValueError를 내고 이 분기에서 400 {"detail":"Invalid CSRF request context"}가 반환됩니다. 그러나 같은 변경의 requirements/API 계약은 scope 검증을 포함한 모든 CSRF 거부를 403 auth.csrf_validation_failed 고정 envelope과 bounded denial reason으로 규정하므로, 이 입력만 Client 오류 판별과 CSRF 관측 경계를 우회합니다. 잘못된 scope를 token 발급 전에 CSRF denial reason으로 변환해 다른 검증 실패와 동일한 고정 응답·감사 경로로 처리해야 합니다.
AGENTS.md reference: docs/AGENTS.md:L47-L49
Useful? React with 👍 / 👎.
| try: | ||
| record_csrf_bootstrap_denial( | ||
| bootstrap_denial, | ||
| request_id=getattr(request.state, "request_id", None), | ||
| ) |
There was a problem hiding this comment.
Bootstrap 거부 감사에도 bounded 경계를 적용하세요
Custom header가 없는 ambient cross-site GET /auth/csrf도 이 분기에 도달하며, 공격자는 응답을 읽지 못해도 cache-busting URL로 요청을 반복할 수 있습니다. 이때마다 record_csrf_bootstrap_denial이 독립 SQLAlchemy session을 열어 PostgreSQL outbox commit을 완료할 때까지 기다리는데, middleware 거부 경로와 달리 전용 CapacityLimiter를 거치지 않고 FastAPI의 공용 sync worker를 사용하므로 반복 GET이 DB connection과 worker를 점유해 정상 로그인·workflow 요청을 지연시킬 수 있습니다. Bootstrap 거부 감사도 같은 bounded telemetry 실행 경계로 보내야 합니다.
AGENTS.md reference: AGENTS.md:L193-L198
Useful? React with 👍 / 👎.
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 32dc308417
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| _REASONS = frozenset( | ||
| { |
There was a problem hiding this comment.
organization scope 거부 reason을 보존하세요
잘못된 X-Organization-Id로 CSRF bootstrap이 거부되는 경우 record_csrf_bootstrap_denial()은 organization_scope_invalid를 전달하지만, 이 allowlist에 해당 값이 없어 metric과 auth.csrf_denied 로그가 모두 unknown으로 기록됩니다. Accepted ADR과 Auth 계약이 이 값을 bounded reason으로 요구하므로, malformed scope 공격이나 클라이언트 오류를 다른 미등록 오류와 구분해 모니터링할 수 없습니다. _REASONS에 organization_scope_invalid를 추가하고 실제 metric label을 검증해야 합니다.
AGENTS.md reference: AGENTS.md:L55-L64
Useful? React with 👍 / 👎.
| issued = token_service.issue( | ||
| binding_kind=binding_kind, | ||
| binding_secret=binding_secret, | ||
| organization_scope=organization_scope, | ||
| ) |
There was a problem hiding this comment.
다른 탭의 bootstrap으로 기존 토큰을 무효화하지 마세요
같은 사용자·origin에서 탭 A가 토큰을 메모리에 보관한 뒤 탭 B가 최초 mutation이나 만료 갱신을 수행하면, 이 코드는 매번 새 nonce의 토큰을 발급해 브라우저 전체가 공유하는 csrf_token cookie를 덮어씁니다. 탭 A는 계속 이전 메모리 토큰을 header로 보내므로 header/cookie mismatch로 403을 받고, 일반 workflow 저장·실행 같은 POST/PATCH는 자동 재시도도 하지 않아 정상 작업이 실패합니다. 기존 cookie가 같은 session/scope에 유효하면 재사용하거나 탭 간 발급을 조정해 공유 cookie와 탭별 메모리가 어긋나지 않게 해야 합니다.
AGENTS.md reference: AGENTS.md:L191-L198
Useful? React with 👍 / 👎.
|
@codex review |
|
Codex Review: Didn't find any major issues. Keep them coming! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
변경 사항
cookie_authenticated,pre_auth_session,public_anonymous,server_credential정책으로 분류하고, 미분류·중복·stale route가 있으면 시작 또는 architecture test에서 실패하도록 했습니다.GET /api/v1/auth/csrfsigned double-submit bootstrap과 auth/anonymous binding, organization scope, 10분 expiry, 로그인·회원가입·OAuth·로그아웃 lifecycle을 구현했습니다.도입 이유는 credentialed CORS와
SameSite=Nonecookie만으로는 cross-site mutation을 차단할 수 없고, 기존 route별 인증 dependency만으로는 login·legacy helper·multipart·Public/server credential 예외를 빠짐없이 통제하기 어렵기 때문입니다.관련 이슈
변경 유형
테스트
검증 결과:
tsc --noEmit: 통과보호 리소스·외부 실행 경계 (해당 시)
docs/decisions/ADR-0073-cookie-authenticated-api-csrf-boundary.md,docs/features/auth/{requirements,api_spec,component_spec,test_cases}.mdapps/gateway/application/csrf/token.py,apps/gateway/tests/application/csrf/test_token_service.py,apps/gateway/tests/api/test_auth_csrf.pyapps/gateway/middleware/csrf.py,apps/gateway/composition/csrf.py,apps/gateway/tests/middleware/test_csrf_*.py,apps/gateway/tests/architecture/test_csrf_route_inventory.pyapps/client/lib/csrfToken.ts,apps/client/lib/csrfToken.test.ts,apps/client/app/stream-api/workflows/[workflowId]/route.test.ts스크린샷 (UI 변경 시)