Skip to content

fix(rate-limit): fail closed without trusted client identity - #47

Merged
seonghobae merged 17 commits into
mainfrom
fix/fail-closed-client-identity
Aug 3, 2026
Merged

fix(rate-limit): fail closed without trusted client identity#47
seonghobae merged 17 commits into
mainfrom
fix/fail-closed-client-identity

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • reject /exchange requests before Durable Object lookup when Cloudflare's CF-Connecting-IP is missing, malformed, or overlong
  • remove the shared unknown fallback bucket that could let one identity-stripped traffic source exhaust the request budget for unrelated callers
  • preserve X-Forwarded-For/X-Real-IP distrust and existing distributed limiter fail-closed responses
  • add integration and branch coverage plus deployment/rollback guidance and changelog evidence
  • pin the transitive Wrangler/Miniflare undici dependency to patched 7.29.0 after current advisories correctly failed the release audit

Commercial-readiness impact

Cloudflare documents CF-Connecting-IP as the edge-provided client identity, but request and managed transforms can remove it. Treating missing identity as one global bucket created an avoidable availability boundary: malformed or misconfigured traffic could consume the shared budget and deny legitimate buyers' automation. The new behavior fails closed with 503 and Retry-After: 1 before token parsing or Durable Object invocation, making the ingress misconfiguration explicit and bounded.

During verification, npm audit --audit-level=high detected newly published undici advisories in the locked transitive dependency. The lockfile is regenerated with undici@7.29.0, preserving a zero-high-severity release gate rather than bypassing the audit.

Verification

  • npm run typecheck
  • npm test
  • npm run security:scan
  • npm run release:verify

References

  • Cloudflare HTTP headers: CF-Connecting-IP
  • Cloudflare managed transform: Remove visitor IP headers
  • GitHub advisories for patched undici@7.29.0

Summary by CodeRabbit

  • 개선 사항

    • /exchange 요청에서 유효한 IPv4·IPv6 식별자만 허용하며 IPv6 주소를 일관되게 정규화합니다.
    • 식별자가 없거나 잘못된 경우 공유 fallback을 사용하지 않고 503 오류로 안전하게 처리합니다.
    • Durable Object 조회 및 인증 정보 파싱 전에 요청 검증을 수행합니다.
  • 보안

    • undici 취약점 대응을 반영했습니다.
  • 배포

    • Node.js 최소 요구 버전을 22 이상으로 변경했습니다.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@seonghobae, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 2 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6a63eeef-13b0-4eac-a3f5-7cc5b5de237a

📥 Commits

Reviewing files that changed from the base of the PR and between c7a9e06 and a3a37da.

📒 Files selected for processing (1)
  • docs/deployment-guide.md
📝 Walkthrough

Walkthrough

/exchangeCF-Connecting-IP를 엄격하게 검증하고 IPv4·IPv6 주소를 canonicalization합니다. 식별자가 없거나 유효하지 않으면 Durable Object 조회 전에 503을 반환합니다. Node.js 22 이상과 undici 7.29.0 고정도 적용했습니다.

Changes

분산 rate limit fail-closed 처리

Layer / File(s) Summary
클라이언트 식별자 및 객체 조회 차단
src/rate-limit.ts
IPv4·IPv6 주소를 검증하고 canonical 식별자를 반환합니다. 유효하지 않은 식별자는 undefined가 되며, 공유 fallback bucket 대신 DistributedRateLimitUnavailable을 발생시킵니다.
503 응답 검증 및 운영 문서
test/distributed-rate-limit.test.ts, docs/distributed-rate-limiting.md
누락·손상·모호한 IP 헤더가 Durable Object 조회 전에 503으로 종료되는 동작을 검증하고 문서화했습니다. IPv6 canonicalization과 운영 검증 절차도 갱신했습니다.

런타임 보안 및 배포 계약

Layer / File(s) Summary
Node.js 요구사항 및 의존성 고정
package.json, docs/deployment-guide.md, CHANGELOG.md
Node.js 요구사항을 >=22로 변경하고, 지원 중인 LTS 사용 조건을 문서화했습니다. undici7.29.0으로 고정하고 관련 changelog를 갱신했습니다.

Estimated code review effort: 2 (Simple) | ~10 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client as 클라이언트
  participant Exchange as /exchange
  participant Identifier as trustedClientIdentifier
  participant RateLimit as distributedRateLimitObjectName
  participant DurableObject as Durable Object

  Client->>Exchange: 요청 전송
  Exchange->>Identifier: CF-Connecting-IP 검증 및 정규화
  Identifier-->>Exchange: canonical IP 또는 undefined
  alt 유효한 식별자
    Exchange->>RateLimit: 객체 이름 생성
    RateLimit->>DurableObject: rate limit 조회
    DurableObject-->>Exchange: rate limit 결과
    Exchange-->>Client: 요청 처리 결과
  else 유효하지 않은 식별자
    RateLimit-->>Exchange: DistributedRateLimitUnavailable
    Exchange-->>Client: 503 응답
  end
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목은 신뢰할 수 있는 클라이언트 식별자가 없을 때 rate limit이 fail-closed로 동작하는 주요 변경을 정확히 설명합니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/fail-closed-client-identity

Comment @coderabbitai help to get the list of available commands.

Comment thread .github/workflows/refresh-undici-lock.yml Fixed
Comment thread .github/workflows/refresh-undici-lock.yml Fixed

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🧹 Nitpick comments (1)
CHANGELOG.md (1)

43-43: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

postcss 보안 수정 항목을 하나로 통합하세요.

Line 43은 Line 21과 동일한 postcss 버전, GHSA, overrides 수정 내용을 기록합니다. security:scan 세부 내용이 필요하면 Line 21에 통합하고 중복 항목을 제거하세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` at line 43, Remove the duplicate postcss security entry at the
referenced changelog location and retain a single consolidated entry with the
postcss version, GHSA, overrides change, and security:scan details in the
existing earlier changelog item.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/distributed-rate-limiting.md`:
- Around line 89-90: Update the rollback guidance around line 87 to allow
rollback only to releases that preserve fail-closed behavior for missing or
invalid CF-Connecting-IP identities. Explicitly prohibit rollback to versions
using a shared unknown fallback bucket; otherwise retain the current entrypoint
and repair the Cloudflare route or transform configuration first.

In `@package.json`:
- Around line 42-43: Declare the Node.js minimum version as >=22 by adding the
engines.node setting in package.json, and update docs/deployment-guide.md to
state Node.js 22+ instead of 20+. Ensure both the package metadata and
deployment documentation consistently reflect the wrangler and undici runtime
requirements.

In `@src/rate-limit.ts`:
- Around line 60-67: Update trustedClientIdentifier to parse CF-Connecting-IP as
exactly one valid IPv4 or IPv6 address using a Workers-compatible validator,
rejecting malformed, out-of-range, comma-separated, and arbitrary values with
undefined before Durable Object lookup. Return a canonical normalized
representation so equivalent IPv6 spellings share the same rate-limit bucket,
and add regression tests covering valid addresses, range errors, multiple
addresses, arbitrary strings, and IPv6 normalization.

In `@test/distributed-rate-limit.test.ts`:
- Around line 247-277: Update the test “fails closed before object lookup
without a bounded Cloudflare client identity” to pass an observedNames
collection from namespaceReturning() into the runtime environment, then assert
it remains empty after all requests. Keep the existing handler assertion and
response contract checks so the test verifies neither idFromName() nor the
Durable Object fetch is invoked.

---

Nitpick comments:
In `@CHANGELOG.md`:
- Line 43: Remove the duplicate postcss security entry at the referenced
changelog location and retain a single consolidated entry with the postcss
version, GHSA, overrides change, and security:scan details in the existing
earlier changelog item.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d1c7eec7-1d12-41f3-b350-b29608b02d02

📥 Commits

Reviewing files that changed from the base of the PR and between ec53bff and 0f07d5c.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • CHANGELOG.md
  • docs/distributed-rate-limiting.md
  • package.json
  • src/rate-limit.ts
  • test/distributed-rate-limit.test.ts

Comment thread docs/distributed-rate-limiting.md Outdated
Comment thread package.json
Comment thread src/rate-limit.ts
Comment thread test/distributed-rate-limit.test.ts Outdated
@seonghobae
seonghobae merged commit 4e5c70e into main Aug 3, 2026
15 of 16 checks passed
@seonghobae
seonghobae deleted the fix/fail-closed-client-identity branch August 3, 2026 21:06
seonghobae pushed a commit that referenced this pull request Aug 3, 2026
…% gate

The main-merge added canonicalIpv6, whose two post-parse guards cannot trigger:
a successful `new URL("http://[cand]/")` parse only occurs for a valid IPv6
literal, so the WHATWG hostname is always bracketed and its inner value always
contains ":". Empirically verified across 133,888 gate-passing candidates
(exhaustive length 1-4 over the allowed alphabet plus adversarial/random
inputs): every one of the 2,396 that parsed stayed bracketed with a ":"-bearing
inner. The bracketed-check and ":"-check guards are kept as fail-closed
backstops and marked `/* v8 ignore next */` so the restored 100% threshold
holds. Line 84's ternary is rewritten as an equivalent guard clause so its
covered happy-path return stays measured; behavior is unchanged.

Verification: npx vitest run --coverage (100% statements/branches/functions/
lines, gate green), npm run typecheck, npm test (268 pass), npm audit
(0 vulnerabilities). No change to #47 rate-limit logic, the threshold, or the
undici override/engines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SEAGtwNR96cia2djq7XFCo
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.

2 participants