security: admin BFF 인가 통일, CSP 강화, URL 스킴 검증, 로그인 레이트리밋 - #44
Conversation
…e limit - Unify admin-role checks across BFF routes via requireAdminRequest() (review-inbox, kb-candidates*, cs-playground, error-report, auth/token) - Validate user-controlled URLs with sanitizeUrl() before rendering as links/window.open (instagramUrl, post.url, target_url, notice url, mission postUrl, dashboard actionUrl) - Tighten CSP: drop unsafe-eval in prod, restrict connect-src to API + socket origins, drop http: img-src, add frame-ancestors 'none', HSTS + Permissions-Policy in prod - Rate-limit /api/admin/auth/login (IP 30/5min, IP+email 10/5min -> 429) - session/country: same-origin guard + normalizeAdminCountry whitelist - refresh: same-origin guard; login response no longer returns accessToken - kb-candidates id param validated before URL interpolation - Remove dead establish-session route and unused axios dep Co-Authored-By: Newbie Smart <smartnewb2@gmail.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5fb948f699
ℹ️ 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 forwarded = headers.get('x-forwarded-for'); | ||
| if (forwarded) return forwarded.split(',')[0].trim(); |
There was a problem hiding this comment.
문서화된 Vercel 프록시 배포에서 요청자가 제공한 X-Forwarded-For가 보존되거나 실제 IP 앞에 추가되는 경우, 현재처럼 첫 번째 값을 사용하면 공격자가 헤더를 매 요청마다 바꿔 IP 제한과 IP+이메일 제한을 모두 우회할 수 있습니다. 플랫폼이 덮어쓰는 신뢰 가능한 IP 헤더를 사용하거나 신뢰한 프록시가 추가한 위치의 값을 선택해야 합니다.
AGENTS.md reference: AGENTS.md:L35-L42
Useful? React with 👍 / 👎.
| if (live.length === 0) buckets.delete(k); | ||
| else buckets.set(k, live); | ||
| } | ||
| if (buckets.size > MAX_TRACKED_KEYS) buckets.clear(); |
There was a problem hiding this comment.


Summary
수동 보안 리뷰(
security-review-2026-09-21.md, 세션 첨부)에서 나온 수정 사항 중 Next.js 업그레이드를 제외한 전체를 반영합니다. 백엔드(sometimes-api) 변경 없음.인가 통일 (M1): 개별 BFF 라우트가
admin_access_token(평문·서명 없는 쿠키) 존재만 확인하던 것을 proxy와 동일한 admin role 검증으로 통일. 공통 헬퍼requireAdminRequest()(shared/auth/require-admin.ts) 추가 — 세션 메타 없으면 401,isAdminRoleSet(meta.roles)실패 시 403, 토큰 없으면 401. 적용:review-inbox,kb-candidatesGET/[id]/approve/[id]/reject,auth/token.cs-playground,error-report에는 동일 검사 인라인 추가(토큰을 안 쓰는 라우트라).URL 스킴 검증 (M3):
sanitizeUrl()(shared/lib/safe-url.ts) — 상대 경로 또는http(s):만 허용,javascript:등은 null. 유저 제어 URL이 어드민 링크로 렌더링되는 지점에 적용:UserAppearanceTable/UserDetailModal(instagramUrl),reports-v2(증빙 이미지openExternalUrl헬퍼),x-marketing(post.url, target_url),NoticeTable,eta-mission-review(postUrl),ActionableInsights(actionUrl).CSP/헤더 (M2):
next.config.js— 프로덕션에서unsafe-eval제거(개발은 React Refresh 때문에 유지),connect-src를'self'+NEXT_PUBLIC_API_URL/NEXT_PUBLIC_SOCKET_URL오리진(+wss)으로 축소,img-src에서http:제거,frame-ancestors 'none',Permissions-Policy, 프로덕션 HSTS 추가.unsafe-inline은 Next bootstrap 스크립트 때문에 유지(nonce 도입은 별도 작업).로그인 레이트리밋 (M4):
shared/lib/rate-limit.ts슬라이딩 윈도 구현 —/api/admin/auth/login에 IP당 30회/5분 + IP+이메일당 10회/5분, 초과 시 429 +Retry-After. 테스트 환경(NODE_ENV=test)에서는 비활성.기타:
session/country에 same-origin 가드 +normalizeAdminCountry화이트리스트 적용(임의 국가값 주입 차단),auth/refresh에 same-origin 가드, 로그인 응답 JSON에서accessToken제거(쿠키로만 전달),kb-candidates [id]를/^[\w-]+$/검증 후 URL 삽입, dead routeestablish-session삭제, 미사용axios의존성 제거.Checks
pnpm typecheck:admin-v2✓,pnpm lint:admin-v2✓ (0 errors),pnpm test:admin✓ 123/123,pnpm build✓Material limits / not done
admin_access_token자체는 여전히 평문 쿠키 — 위 방어는 "존재 확인"이 아니라 "복호화된 세션 메타 + role" 기준이라 변조된 토큰 쿠키는 backend 401로 종결됨go/prefix는 백엔드 경로 확인 불가로 미조정; staging http 전용 secure-cookie 이슈는 해당 환경 부재로 미적용Link to Devin session: https://app.devin.ai/sessions/28aeed4402ec4a02a3f6a6138f4899d0
Open in Devin Desktop: https://app.devin.ai/desktop/session/28aeed4402ec4a02a3f6a6138f4899d0?variant=devin
Requested by: @Smartnewb