Skip to content

security: CSP nonce, 레이트리밋 영속화, 의존성 취약점 정리 - #46

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

Smartnewb merged 1 commit into
mainfrom
codex/security-hardening-2

Conversation

@Smartnewb

Copy link
Copy Markdown
Owner

Summary

이전 보안 리뷰(#44, #45) 이후 남아있던 항목들 정리.

CSP nonce (middleware.ts + shared/lib/csp.ts)

  • 프로덕션 script-src에서 'unsafe-inline' 제거. 요청마다 crypto.getRandomValues로 nonce 생성 → request CSP 헤더에 심으면 Next.js가 자기 bootstrap/inline 스크립트에 nonce를 적용하고, 'strict-dynamic'이 nonce'd 스크립트가 로드하는 webpack chunk를 허용.
  • dev는 React Refresh 때문에 unsafe-inline/unsafe-eval 유지. /api/* 라우트는 CSP 미적용(HTML 응답 아님).
  • next.config.js의 정적 CSP 제거, 나머지 보안 헤더(HSTS prod, XFO, XCTO, Referrer-Policy, Permissions-Policy) 유지.

로그인 레이트리밋 영속화 (shared/lib/rate-limit.ts)

  • UPSTASH_REDIS_REST_URL/UPSTASH_REDIS_REST_TOKEN env가 있으면 Upstash REST pipeline(INCR + PEXPIRE NX + PTTL)으로 인스턴스 간 공유 카운팅 — Vercel 멀티인스턴스에서 실효성 확보.
  • env 없거나 fetch 실패 시 기존 인메모리 슬라이딩 윈도우로 fail-open — 현재 배포에선 env 미설정이라 동작 변화 없음. 실효성 확보하려면 Upstash 생성 후 env 등록 필요.

의존성 취약점: pnpm audit --prod 73건 → 1건

  • dompurify, nanoid 범프 + pnpm.overrides 20건(form-data, socket.io-parser, ws, lodash, js-yaml, glob/minimatch/picomatch/brace-expansion, postcss 계열, babel, fflate, yaml 등).
  • node-cron 제거 — 코드에서 미사용이던 dead dependency(uuid@8 CVE 경유).
  • 남은 1건: quill@2.0.3 XSS via HTML export(GHSA-v3m3-f69x-jf25, low) — upstream 패치 없음. 어드민 전용 에디터이고 입력이 관리자 본인이라 실위험 낮음; 패치 나오면 범프.

버그 수정: /admin/scheduled-matching 지역 지도 — adminGet이 {data:...} 봉투를 그대로 반환해 mapStats가 항상 빈 값(기존 버그, E2E에서 발견).

검증

  • pnpm typecheck:admin-v2 ✅
  • pnpm lint:admin-v2 0 errors(기존 [REDACTED SECRET]s만) ✅
  • pnpm test:admin 123/123 ✅
  • pnpm build ✅
  • pnpm audit --prod 1 low(quill, 패치 없음)

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

…region-map fix

- CSP: move to per-request nonce in middleware (prod drops unsafe-inline,
  adds strict-dynamic); dev keeps unsafe-eval for React Refresh
- Rate limit: Upstash Redis REST when UPSTASH_REDIS_REST_* envs are set,
  in-memory sliding window fallback; login route awaits async check
- Deps: pnpm overrides + bumps clear 72/73 audit findings (73 -> 1);
  node-cron removed (unused); only quill XSS remains (no patch upstream)
- Fix: scheduled-matching region map always empty - unwrap {data} envelope

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 Building Building Preview Sep 21, 2026 4:53am 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-21T05:02:07.555215Z 8212c19 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

@Smartnewb
Smartnewb merged commit 0d5a2af into main Sep 21, 2026
2 of 3 checks passed

@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: 8212c19233

ℹ️ 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/csp.ts
"default-src 'self'",
isDev
? "script-src 'self' 'unsafe-inline' 'unsafe-eval'"
: `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 정적 페이지를 전환한 뒤 nonce CSP를 켜세요

프로덕션에서는 요청별 nonce가 있는 스크립트만 허용하지만, app/layout.tsx와 app/page.tsx에는 dynamic = 'force-dynamic', connection() 또는 서버 동적 API 사용이 없습니다. 미사용 상태인 app/force-dynamic.ts도 Next.js가 인식하는 세그먼트 설정 파일이 아니므로 로그인 페이지는 정적으로 사전 렌더링될 수 있고, 이때 bootstrap 스크립트에는 현재 요청의 nonce가 없어 CSP에 차단됩니다. 그 결과 로그인 폼이 hydration되지 않으므로 실제 layout/page를 동적 렌더링으로 전환한 뒤 nonce 전용 정책을 적용해야 합니다.

AGENTS.md reference: AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

Comment thread package.json
Comment on lines +121 to +122
"@babel/core": ">=7.29.6",
"@babel/plugin-transform-modules-systemjs": ">=7.29.4",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Babel override를 Node 20 호환 버전으로 제한하세요

CI의 verify 잡은 .github/workflows/ci.yml:29에서 Node 20을 고정해 설치 후 빌드와 테스트를 실행하지만, 이 하한만 있는 override 때문에 frozen lockfile은 Node ^22.18.0 || >=24.11.0만 지원하는 @babel/core@8.0.6과 plugin 8.0.1을 선택합니다(pnpm-lock.yaml:317-319, 741-743). 따라서 CI가 명시적으로 지원 범위 밖인 Babel 툴체인을 실행하게 되므로 override를 호환되는 7.x 버전으로 제한하거나 CI와 배포 Node를 함께 올려야 합니다.

AGENTS.md reference: AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

Comment thread package.json
"nanoid": "^5.1.16",
"next": "15.5.25",
"node-cron": "^3.0.3",
"node-fetch": "^3.3.2",

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 matching 스크립트의 node-cron 의존성을 복구하세요

새로 설치한 환경에서 pnpm matching을 실행하면 scripts/matching-cron.ts:1이 이 커밋으로 제거된 node-cron을 import하므로 작업을 등록하기 전에 module-not-found로 종료됩니다. package.json:13의 명령과 해당 스크립트가 남아 있는 동안에는 런타임 의존성을 유지하거나 명령과 스크립트를 함께 제거해야 합니다.

AGENTS.md reference: AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

Comment thread shared/lib/rate-limit.ts
max: number,
): Promise<RateLimitResult | null> {
try {
const res = await fetch(`${UPSTASH_URL}/pipeline`, {

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 Redis 요청에 fallback용 타임아웃을 설정하세요

Upstash가 설정된 환경에서 REST 연결이나 응답이 정지하면 이 fetch에는 timeout이나 AbortSignal이 없어 플랫폼의 요청 제한에 도달할 때까지 로그인 처리가 대기합니다. 현재 catch 기반 인메모리 fallback은 fetch가 실제로 reject된 뒤에만 실행되므로 이 상황에서는 로그인이 hang 또는 504로 끝납니다. 짧은 명시적 타임아웃 후 fallback하도록 해야 합니다.

AGENTS.md reference: AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

Comment thread shared/lib/rate-limit.ts
Comment on lines +29 to +31
['INCR', key],
['PEXPIRE', key, windowMs, 'NX'],
['PTTL', key],

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 Redis에서도 슬라이딩 윈도우 상한을 유지하세요

이 구현은 첫 INCR 때만 TTL을 설정하므로 기존 인메모리 슬라이딩 윈도우가 아니라 고정 윈도우로 동작합니다. 예를 들어 5분당 10회 제한에서 최초 요청으로 윈도우를 연 뒤 만료 직전에 9회, 만료 직후 10회를 보내면 실제 연속 5분 동안 19회가 허용되어 Upstash 사용 시 로그인 brute-force 상한이 거의 두 배로 약해집니다. sorted set 등으로 rolling window를 유지해야 합니다.

AGENTS.md reference: AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

Comment thread shared/lib/rate-limit.ts
Comment on lines +35 to +37
const results = (await res.json()) as Array<{ result?: number }>;
const count = Number(results[0]?.result ?? 0);
const ttlMs = Number(results[2]?.result ?? windowMs);

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 명령 단위 pipeline 오류를 store 실패로 처리하세요

Upstash pipeline은 HTTP 요청 자체가 성공해도 개별 명령 결과에 error가 포함될 수 있는데, 현재 파서는 실패한 INCR의 누락된 result를 0으로 바꿔 요청을 허용하고 인메모리 fallback도 실행하지 않습니다. 반대로 INCR만 성공하고 PEXPIRE가 실패하면 만료 없는 키가 남아 제한 초과 후 로그인이 계속 차단될 수 있으므로, 세 pipeline 항목의 오류와 숫자 결과를 모두 검증하고 하나라도 실패하면 store 실패로 처리해야 합니다.

AGENTS.md reference: AGENTS.md:L9-L9

Useful? React with 👍 / 👎.

This branch was successfully deployed

1 active deployment
Preview — 8212c192 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