Skip to content

security: admin BFF 인가 통일, CSP 강화, URL 스킴 검증, 로그인 레이트리밋 - #44

Merged
Smartnewb merged 1 commit into
mainfrom
codex/security-hardening
Sep 21, 2026
Merged

Smartnewb merged 1 commit into
mainfrom
codex/security-hardening

Conversation

@Smartnewb

Copy link
Copy Markdown
Owner

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-candidates GET/[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 route establish-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로 종결됨
  • proxy allowlist의 go/ prefix는 백엔드 경로 확인 불가로 미조정; staging http 전용 secure-cookie 이슈는 해당 환경 부재로 미적용
  • Next.js 14.2.35 CVE 업그레이드(H1)는 별도 PR
  • 테스트 업데이트는 변경된 라우트 계약에 맞춘 목/요청 조정뿐(새 가드 우회용 변경 아님)

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

…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>
@Smartnewb Smartnewb self-assigned this Sep 21, 2026
@vercel

vercel Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
project-solo Ready Ready Preview Sep 21, 2026 2:08am UTC

Request Review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-21T02:11:57.284447Z 5fb948f PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
5.7% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread shared/lib/rate-limit.ts
Comment on lines +45 to +46
const forwarded = headers.get('x-forwarded-for');
if (forwarded) return forwarded.split(',')[0].trim();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 신뢰 가능한 헤더에서 클라이언트 IP를 가져오세요

문서화된 Vercel 프록시 배포에서 요청자가 제공한 X-Forwarded-For가 보존되거나 실제 IP 앞에 추가되는 경우, 현재처럼 첫 번째 값을 사용하면 공격자가 헤더를 매 요청마다 바꿔 IP 제한과 IP+이메일 제한을 모두 우회할 수 있습니다. 플랫폼이 덮어쓰는 신뢰 가능한 IP 헤더를 사용하거나 신뢰한 프록시가 추가한 위치의 값을 선택해야 합니다.

AGENTS.md reference: AGENTS.md:L35-L42

Useful? React with 👍 / 👎.

Comment thread shared/lib/rate-limit.ts
if (live.length === 0) buckets.delete(k);
else buckets.set(k, live);
}
if (buckets.size > MAX_TRACKED_KEYS) buckets.clear();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 용량 초과 시 활성 레이트리밋을 모두 초기화하지 마세요

한 인스턴스에서 5분 내 서로 다른 IP·이메일 조합이 충분히 유입되어 5,000개를 넘으면 아직 유효한 버킷까지 clear()되어 차단 중이던 로그인 시도가 즉시 다시 허용됩니다. 로그인 요청 하나가 IP와 계정 버킷을 각각 만들기 때문에 분산 공격이나 트래픽 급증 시 이 경로를 반복적으로 유발할 수 있으므로, 전체 초기화 대신 오래된 항목 또는 제한된 수의 항목만 퇴출해야 합니다.

Useful? React with 👍 / 👎.

@Smartnewb
Smartnewb merged commit 5ed2612 into main Sep 21, 2026
3 of 4 checks passed
@Smartnewb
Smartnewb deleted the codex/security-hardening branch September 21, 2026 04:02

This branch was successfully deployed

1 active deployment
Preview — 5fb948f6 Deployed Sep 21, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant