test(web): pin both edges of the SSRF guard's IPv6 bit-mask ranges - #1512
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughSummary by CodeRabbit
WalkthroughThe PR adds SSRF guard tests for IPv6 site-local, discard, and NAT64 boundaries. It covers literal addresses, nearby allowed addresses, and DNS-resolved IPv6 addresses. Test setup mocks DNS resolution and restores mocks and console output after each test. ChangesSSRF boundary coverage
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related issues
Poem
🚥 Pre-merge checks | ✅ 5 | ❌ 2❌ Failed checks (2 inconclusive)
✅ Passed checks (5 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
✨ Simplify code
Warning Review ran into problems🔥 ProblemsThese MCP integrations need to be re-authenticated in the Integrations settings: Sentry Linked repositories: Public OSS repositories can only analyze public repositories installed in this organization. No linked repositories were analyzed; skipped Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review Labels added manually ( Worth reviewing with fresh eyes rather than treating your confirmation on #1486 as pre-approval: this is a different diff. That thread confirmed the gap; what's here is a specific implementation of the fix, including both corrections you specified (exact Generated by Claude Code |
|
|
Superseded by #1486, which merged at 21:23 — and one test here now contradicts
|
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Snapshot WarningsEnsure that dependencies are being submitted on PR branches and consider enabling retry-on-snapshot-warnings. See the documentation for more information and troubleshooting advice. Scanned FilesNone |
Closes #1520 `ssrf-guard.test.ts` covers the too-narrow direction for every blocked range — each has an address squarely inside it. Nothing covers too-wide. `fec0::/10` and `100::/64` are the two checks written as bit masks rather than exact matches, and a widened mask fails silently in the opposite direction: it refuses public space, reporting the same uniform message every other rejection uses. Neither edge of either mask was pinned, so widening one broke no test. Adds both edges of each range plus a just-outside neighbour, through both paths into `ipIsPrivate` — URL literals and DNS answers enter via different branches of `assertPublicHttpUrl` and report different messages, so a regression can land on one and not the other. Also covers `64:ff9b::169.254.169.254`, the dotted-quad spelling of a case `ssrf-guard.test.ts` already has in hex. `ipv6ToHextets` folds the trailing quad before any range check runs, so both spellings must agree. These pass on `main` by construction — there is no bug here. Non-vacuity is therefore shown by mutation instead, each mutant caught by exactly the test that claims to cover it: (h[0] & 0xffc0) === 0xfec0 -> (h[0] & 0xff00) === 0xfe00 fails: still allows [fe00::1], which is below the range h[0] === 0x0100 && h[1..3] === 0 -> h[0] === 0x0100 fails: still allows [100:0:0:1::1] (inside 100::/16, outside the /64) Tests only. Full apps/web suite 59 files / 354 passed; tsc and eslint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NNXdtee4Wwwjew74cNZ1C5
8ac3958 to
2d65d6d
Compare
|
Green on All required checks pass: Two notes on reading that check list, since both could mislead:
Also worth flagging: No auto-merge performed. CodeRabbit has not reviewed this head — it was rate-limited through the window and its auto-review is label-gated. Generated by Claude Code |
|
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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 `@apps/web/src/lib/__tests__/ssrf-guard-range-boundaries.test.ts`:
- Around line 68-72: Extend the SSRF boundary tests around rejectionOf to also
reject the final address in the 100::/64 range, using the literal
100:0:0:0:ffff:ffff:ffff:ffff and asserting the existing “Blocked private IP
literal” message.
- Around line 37-39: Update the catch block in the affected test helper to
narrow the unknown caught value with an instanceof Error check before returning
it; rethrow non-Error values so the Promise<Error> contract remains reliable.
🪄 Autofix
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: Repository YAML (base), Repository UI (inherited), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: e0beeef8-5b48-4968-a0a9-d5bb0226363a
📒 Files selected for processing (1)
apps/web/src/lib/__tests__/ssrf-guard-range-boundaries.test.ts
📜 Review details
⚠️ CI failures not shown inline (1)
GitHub Check: PR Governance: Canonical delivery contract blocked
Conclusion: failure
exactly one closing reference is required: Closes #<issue>
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{py,js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{py,js,jsx,ts,tsx}: Use Python 3.9+ and Node 18+ for development
Never hardcode API keys, database URLs, or secrets in code
Make minimal, surgical changes and avoid deleting working code unless fixing security issues
Files:
apps/web/src/lib/__tests__/ssrf-guard-range-boundaries.test.ts
**/*.{py,js,ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Maintain >80% code coverage for new features
Files:
apps/web/src/lib/__tests__/ssrf-guard-range-boundaries.test.ts
**/*.{py,ts,tsx}
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.{py,ts,tsx}: Keep frontend and backend data models synchronized using matching Pydantic (backend) and TypeScript (frontend) interfaces
Use type-safe interfaces for backend-frontend data exchange
**/*.{py,ts,tsx}: Production code must use real behavior only: no mock delays, fake data, or simulated responses.
Maintain strict type safety: mypy strict mode for Python and TypeScript strict mode for the frontend.
Name events using the<domain>.<entity>.<action>format.
Files:
apps/web/src/lib/__tests__/ssrf-guard-range-boundaries.test.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursorrules)
**/*.{ts,tsx}: TypeScript runs in strict mode: do not use implicitanyor unsafe assertions.
Allfetch()calls must useAbortSignal.timeout().
Do not use a barecatch; errors must be logged when caught.Use strict TypeScript mode.
Files:
apps/web/src/lib/__tests__/ssrf-guard-range-boundaries.test.ts
**/*
📄 CodeRabbit inference engine (Custom checks)
**/*: Strictly verify that GitHub Copilot has explicitly reviewed and approved the pull request; human approvals alone must not satisfy this check.
Before allowing a merge, require thecopilot-rabbitlabel and AI-generated unit tests committed alongside the code changes; fail the check if either is missing.For Vercel-specific work, include
https://vercel.com/docs/llms-full.txtin the AI assistant context set.
Files:
apps/web/src/lib/__tests__/ssrf-guard-range-boundaries.test.ts
apps/web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
apps/web/**/*.{ts,tsx}: Use strict TypeScript mode.
Use ESLint with the shared Next.js configuration.
Use Tailwind CSS for styling.
Use the@/*path alias for imports mapped tosrc/*.
Do not usedangerouslySetInnerHTMLin React.
apps/web/**/*.{ts,tsx}: Use the Next.js ESLint rules and the shared configuration frompackages/eslint-config/.
Use the@/*path alias for imports mapped toapps/web/src/*.
Never usedangerouslySetInnerHTML.
Files:
apps/web/src/lib/__tests__/ssrf-guard-range-boundaries.test.ts
**/*.{py,ts,tsx,js,jsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Do not commit secrets; store keys and credentials in gitignored
.envfiles.
Files:
apps/web/src/lib/__tests__/ssrf-guard-range-boundaries.test.ts
**/*.{py,pyi,ts,tsx}
📄 CodeRabbit inference engine (GEMINI.md)
**/*.{py,pyi,ts,tsx}: Preserve the single workflow: YouTube link → transcript → events → agents → outputs; do not introduce alternative flows or manual triggers that bypass it.
Use event names following<domain>.<entity>.<action>, such asyoutube.video.captured.
Make surgical, precise changes and do not delete working code without justification.
Files:
apps/web/src/lib/__tests__/ssrf-guard-range-boundaries.test.ts
**/*.ts
⚙️ CodeRabbit configuration file
This is a TypeScript/Next.js project. Focus on type safety, null checks, async/await error handling, and SSE stream lifecycle management. Flag any fetch() calls without AbortSignal.timeout. Check for proper error boundaries. Flag any TODO, placeholder, or stub implementations that are not production-ready. Enforce TypeScript strict mode compliance — flag implicit any, missing return types, and unsafe type assertions.
Files:
apps/web/src/lib/__tests__/ssrf-guard-range-boundaries.test.ts
🔍 Remote MCP GitHub Copilot
Additional review context
- PR
#1512adds onlyssrf-guard-range-boundaries.test.ts(+124 lines); production SSRF logic is unchanged. It covers 9 cases across URL literals, DNS answers, IPv6 boundaries, and dotted-quad NAT64 notation. mainexplicitly checksfec0::/10with(h[0] & 0xffc0) === 0xfec0and100::/64by requiringh[0] === 0x0100plush[1]–h[3] === 0; it also folds embedded IPv4 text before transition-prefix checks. The new assertions match these implementations.- Existing tests already cover interior blocked ranges, public IPv6 controls, DNS resolution, and NAT64 in hexadecimal form. The new file adds the missing mask edges/outside neighbors and dotted-quad NAT64 coverage.
- The current
test-frontend, build, lint, security, CodeQL, coverage, and validation checks are successful. An older “Canonical issue and evidence” failure remains visible, while the later run passed; the overall commit status is still pending because CodeRabbit’s review status is pending. - There are no review threads or substantive completed reviews on the current head.
🔇 Additional comments (1)
apps/web/src/lib/__tests__/ssrf-guard-range-boundaries.test.ts (1)
1-1: 📐 Maintainability & Code QualityBlock merge until required Copilot evidence is present.
PR
#1512still lacks thecopilot-rabbitlabel and an explicit GitHub Copilot approval on the current head. Add both before merge; the AI-attributed test is present, so only the Copilot review evidence remains missing.
Both findings were valid.
1. `err as Error` in the `rejectionOf` helper was an unsafe assertion on a
value `catch` binds as `unknown`, which `.cursorrules` forbids outright.
Narrow with `instanceof` and rethrow anything else, so a non-Error throw
surfaces as itself rather than being mistyped and failing later on a
missing `.message`.
2. The `100::/64` range was pinned only at its first address, `100::`. That
is an inconsistency in this file's own premise: `fec0::/10` gets both
edges, and the whole point of the file is that one edge does not hold a
range. A regression blocking only the first address would have passed.
Add `100:0:0:0:ffff:ffff:ffff:ffff`.
The second finding is confirmed by mutation rather than taken on faith —
replacing the range check with an exact match on `100::` alone:
h[0] === 0x0100 && h[1..3] === 0 -> h.join(':') === '256:0:0:0:0:0:0:0'
fails: rejects [100:0:0:0:ffff:ffff:ffff:ffff] (last address in the range)
That mutant survived the previous version of this file, so the gap was real.
The two mutants already covered still fail exactly one test each.
Full apps/web suite 59 files / 355 passed; tsc and eslint clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNXdtee4Wwwjew74cNZ1C5
Canonical issue
Closes #1520
Outcome
ipIsPrivateblocks two IPv6 ranges with bit masks rather than exact matches:ssrf-guard.test.tscovers the too-narrow direction thoroughly — every blocked range has an address squarely inside it. Nothing covers too-wide, and neither edge of either mask is pinned.That asymmetry matters because a too-wide mask fails silently. It refuses legitimate public destinations, and the only symptom is a rejection carrying the same uniform message every other rejection uses — the indistinguishability property #1381 introduced on purpose. Nothing separates "correctly blocked" from "wrongly blocked" at the call site.
Both plausible slips are within a character of the current code:
0xffc0/0xfec0→0xff00/0xfe00widens site-local tofe00::/8; dropping theh[1]–h[3]conjunction widens the discard prefix from a/64to a/16.Scope
apps/web/src/lib/__tests__/ssrf-guard-range-boundaries.test.ts— new file, 10 tests. Both edges of each masked range, a just-outside neighbour of each, and the dotted-quad spelling of a NAT64 address thatssrf-guard.test.tsalready covers in hex.mainis correct as of fix(web): range-check bracketed IPv6 literals in the SSRF guard #1486. Not one line ofssrf-guard.tschanges; this PR is one new test file and nothing else.Risk
Verification
Head
75a901d, based onmain@f876787.Focused tests — 10 cases, all passing.
Non-vacuity shown by mutation, not by failing against
main. These pass onmainby construction — there is no bug here, which is exactly why the usual "revert the fix and watch it fail" check does not apply. Saying so plainly rather than letting them look like fix-pinning tests. Each mutant is caught by precisely the test that claims to cover it:(h[0] & 0xffc0) === 0xfec0→(h[0] & 0xff00) === 0xfe001 failed | 9 passed— still allows [fe00::1], which is below the rangeh[0] === 0x0100 && h[1..3] === 0→h[0] === 0x01001 failed | 9 passed— still allows [100:0:0:1::1] (inside 100::/16, outside the /64)h[0] === 0x0100 && h[1..3] === 0→ exact match on100::alone1 failed | 9 passed— rejects [100:0:0:0:ffff:ffff:ffff:ffff] (last address in the range)The third mutant is the regression CodeRabbit identified in review. It survived the first version of this file — that gap was real, and closing it is what the second commit does.
Both entry paths covered — URL literals and DNS answers reach
ipIsPrivatethrough different branches ofassertPublicHttpUrland report different messages (Blocked private IP literalvsHost does not resolve to a public address), so a regression can land on one and not the other.Full apps/web suite — 59 files, 355 passed, 0 failed.
npx tsc --noEmit— clean.npm run lint— clean.Required CI green on
75a901d— 26 checks, zero failures:test-frontend,test,build,guards,lint-frontend,lint-python,validate,PR Governance,Canonical issue and evidence, both Security Scans,bandit,trivy,python-safety,npm-audit,gitleaks,dependency-review,CodeQL,Coverage, Vercel.The new tests are genuinely gated, not vacuously green —
ci.yml'stest-frontendrunscd apps/web && npx vitest run, and the job log shows✓ src/lib/__tests__/ssrf-guard-range-boundaries.test.tsby name. A green total alone would not distinguish "ran and passed" from "never collected".Review threads resolved — both CodeRabbit findings addressed in
75a901dand confirmed by CodeRabbit.Review findings addressed
CodeRabbit requested changes on
2d65d6d; both were valid and are fixed in75a901d.err as Errorin therejectionOfhelper — an unsafe assertion on a valuecatchbinds asunknown, which.cursorrulesforbids outright. Narrowed withinstanceofand rethrow, so a non-Error throw surfaces as itself rather than being mistyped and failing later on a missing.message.100::/64was pinned only at its first address. This contradicted the file's own premise —fec0::/10gets both edges, and the entire argument here is that one edge does not hold a range.100:0:0:0:ffff:ffff:ffff:ffffadded, and the mutation above proves it catches the regression.What happened to the original change
Recording this because the conclusion is that I was wrong, and that is worth stating rather than quietly force-pushing over.
This branch originally implemented the transition-prefix fix independently. #1486 landed the same fix first. On comparing them,
main's implementation is a strict superset — and on one case mine was less safe:main(#1486)64:ff9b:1::a00:1— RFC 8215 local-use NAT6464:ff9b:1::/48return false→ allowed64:ff9b, is not the exact/96, → blockedmainblocks the whole of64:ff9b::/32outside the well-known/96, on the reasoning that anything else in that prefix is local-use by definition and guessing where its embedded IPv4 sits would be worse than refusing it. That is the better call. My version had a test asserting the permissive behaviour, which would have encoded the weaker posture into the suite.So the production commits were dropped rather than rebased — merging them would have regressed
main. Only unmerged work of my own was discarded; no merged history was touched. What survives is the partmaingenuinely lacks, rewritten againstmain's actual behaviour.I also filed the finding on #1486 (comment) correcting my own earlier framing of it as "a regression in #1486" — it was reachable on
mainvia DNS answers before #1486 existed.Production evidence
Not applicable — test-only, no runtime behaviour changes and no
apps/webUI surface. Vercel preview builds green.Agent handoff
75a901d.One outstanding item is not something I can satisfy myself: CodeRabbit's
Enforce Copilot Verificationpre-merge check wants an explicit GitHub Copilot approval plus thecopilot-rabbitlabel. I requested a Copilot review rather than adding the label, since adding it myself would assert that a review happened when it has not. If the repo wants that gate satisfied, it needs the Copilot review to actually land.Agent provenance
Produced by a scheduled, unattended PR-remediation routine running under the repo owner's account. It halts at the human gate — no auto-merge to protected
mainis requested or performed. The branch was force-pushed once, replacing two unmerged commits of my own with the rescoped2d65d6d;75a901dthen applied the review fixes on top.