fix(security): block IPv4-compatible IPv6 and NAT64 SSRF bypasses in validateGitUrl#1148
Conversation
Vulnerability: SSRF via IPv6 forms that embed IPv4 addresses Severity: high Location: gitnexus/src/server/git-clone.ts:assertNotPrivateIPv6 validateGitUrl() blocks ::ffff:x.x.x.x (IPv4-mapped) but two related forms still slipped through — both routable to the embedded IPv4 on common stacks: 1. IPv4-compatible IPv6 (RFC 4291 § 2.5.5.1, deprecated): http://[::127.0.0.1]/ — Node's URL parser collapses this to "::7f00:1" with no ::ffff: marker, so the existing check missed it. 2. NAT64 well-known prefix (RFC 6052: 64:ff9b::/96, plus RFC 8215's 64:ff9b:1::/48 local prefix): a host with NAT64 enabled translates 64:ff9b::7f00:1 to 127.0.0.1, reaching loopback. Impact: an attacker who can submit a clone URL to /api/analyze (any caller in the CORS-allowlisted origin set — localhost, RFC 1918 LAN, or gitnexus.vercel.app) could direct git clone at loopback or cloud metadata addresses (169.254.169.254 → ::a9fe:a9fe, 64:ff9b::a9fe:a9fe). Fix: extend assertNotPrivateIPv6 to reject any address compressed to ::xxxx[:yyyy] and any address starting with the NAT64 prefix 64:ff9b:. Tests added for both forms plus the cloud-metadata variants.
|
Someone is attempting to deploy a commit to the NexusCore Team on Vercel. A member of the Team first needs to authorize it. |
|
Claude finished @magyargergo's task in 4m 39s —— View job Security Review — PR #1148: fix(security): block IPv4-compatible IPv6 and NAT64 SSRF bypassesReview tasks:
Review bar for this PR
Issue and PR context considered
Understanding of the changeThe existing
The fix adds two checks inside
Findings[high] Expanded IPv4-compatible IPv6 forms have no test coverage confirming Node normalization
[high] 6to4 (
|
| DoD item | Status |
|---|---|
validateGitUrl() blocks IPv4-compatible IPv6 forms that embed private IPv4 addresses |
partially satisfied — compressed forms blocked; expanded forms untested |
validateGitUrl() blocks NAT64 addresses that can map to loopback, link-local, RFC1918 |
partially satisfied — check correct; RFC1918 embedded variants untested |
| Existing IPv4, IPv4-mapped IPv6, loopback, link-local, unique-local, multicast, unspecified checks remain intact | satisfied — no regressions observed |
| Legitimate public IPv4 and public IPv6 clone URLs continue to pass | partially satisfied — logic is correct; no public IPv6 positive test |
| Tests cover normalized, compressed, expanded, and mixed IPv6 spellings | not satisfied — only compressed forms tested; no expanded/padded/mixed tests |
| Implementation is explicit, maintainable, does not over-block without justification | partially satisfied — NAT64 prefix breadth needs a comment |
/api/analyze and cloneOrPull URL validation boundary correctly enforced |
satisfied — validateGitUrl is called inside cloneOrPull before first clone; pull path uses stored remote, not re-supplied URL |
| CI, typecheck, and focused security tests green before approval | not satisfied — CI in progress |
SSRF validation assessment
IPv4-compatible IPv6 handling: The regex /^::[0-9a-f]{1,4}(:[0-9a-f]{1,4})?$/ correctly blocks compressed Node-normalized forms. Its correctness for expanded inputs depends on WHATWG URL normalization, which the spec mandates but tests do not confirm.
NAT64 handling: startsWith('64:ff9b:') is correct for all currently assigned NAT64 ranges and is defensively broad for the unassigned rest of 64:ff9b::/32. The RFC citation in the comment should be extended to explain the intentional breadth.
Existing private IPv4/IPv6 handling: Loopback, unspecified, ULA (fc/fd), link-local (fe80–feb), IPv4-mapped (::ffff:), and expanded mapped (0:0:0:0:0:ffff:) forms all remain blocked and are unmodified by this PR.
Public IPv6 false-positive behavior: Logically correct — public IPv6 (e.g. 2606:4700:4700::1111) does not match any block pattern. Not test-verified.
Redirect/DNS scope: Not addressed. validateGitUrl() only checks the literal host in the supplied URL. DNS rebinding, DNS resolution to private IPs, and HTTP redirects from public to private hosts are existing limitations not in scope for this PR. This is acceptable for a literal-IP fix, but it means the overall SSRF posture is defense-in-depth, not complete.
Validation boundary for /api/analyze and cloneOrPull:
POST /api/analyze→cloneOrPull(repoUrl, targetPath)→validateGitUrl(url)(only on first clone). Confirmed correct.- On subsequent
git pull,validateGitUrlis not re-called.git pull --ff-onlyuses the remote URL from.git/config, which was validated at clone time. The user-suppliedrepoUrldoes not control the pull destination. Not an SSRF bypass in the current architecture. - Unblocked gap (pre-existing): 6to4 addresses (
2002::/16) embed IPv4 and are not caught by any existing or new check.
Test assessment
Tests added:
- IPv4-compatible:
::127.0.0.1(mixed notation),::7f00:1(hex),::a9fe:a9fe(cloud metadata) - NAT64:
64:ff9b::7f00:1,64:ff9b::a9fe:a9fe,64:ff9b:1::1
Missing variants:
- Expanded IPv4-compatible:
[0:0:0:0:0:0:7f00:1],[0000:0000:0000:0000:0000:0000:7f00:0001],[0:0:0:0:0:0:127.0.0.1]— absence means expanded bypass is unconfirmed - NAT64 with RFC1918 embedded addresses:
64:ff9b::a00:1,64:ff9b::ac10:1,64:ff9b::c0a8:101 - Zero-byte compressed form:
[::2],[::a](to confirm the regex correctly blocks low-numbered addresses)
Positive public-IP tests: Only 140.82.121.4 (IPv4). No public IPv6 positive test.
Regression coverage: Existing tests for all prior block categories are present and unmodified. New tests would fail before the patch (the patterns they test only throw due to the new checks).
Would tests fail before the patch? Yes — the three new it blocks would fail without the added regex and NAT64 check.
Validation assessment
- CI status: "CI" and "Docker Build & Push" workflows are
in_progressfor PR headbcd3cc1. Typecheck and unit tests not yet confirmed green. - Focused test command:
cd gitnexus && npx vitest run test/unit/git-clone.test.ts— not run in this review (CI pending). - Typecheck: Not confirmed (
npx tsc --noEmitnot run). - Anything not verified: Expanded IPv6 normalization behavior (requires running Node); CI results.
- Pending CI blocks approval: Yes, per DoD §4.2 and review instructions.
Final verdict
not production-ready
CI is still in-progress, which is an unconditional merge blocker per both the DoD and the explicit instructions for this review. Beyond CI, the PR does not include tests for expanded IPv4-compatible IPv6 forms (0:0:0:0:0:0:7f00:1, zero-padded), leaving the core correctness assumption about WHATWG URL normalization unverified by tests — for a security-sensitive parser fix this is insufficient. There is also no positive test confirming legitimate public IPv6 clone URLs are unaffected. Finally, the 6to4 gap (2002::/16) is a known IPv4-embedding mechanism that the PR description implies is covered but is not. The core logic (regex + NAT64 prefix) is directionally correct and the compressed forms are properly blocked; the PR needs test additions and CI green before it clears the DoD bar.
|
@aaronjmars Can you please look into these findings? 🙏 |
CI Report✅ All checks passed Pipeline Status
Test Results
✅ All 7800 tests passed 1 test(s) skipped — expand for details
Code CoverageTests
📋 View full run · Generated by CI |
…n tests Address review findings on PR #1148: - Block 6to4 (2002::/16, RFC 3056). The prefix encodes an IPv4 address in bits 17-48, so 2002:7f00:0001::* routes to 127.0.0.1 on 6to4-capable stacks. RFC 7526 deprecated the protocol and the public relay anycast has been retired, so broad-blocking has near-zero false-positive cost. - Expand the NAT64 comment to justify the broader-than-CIDR check: the whole 64:ff9b::/32 block is IANA-reserved for IPv4-IPv6 translation, so a future narrower CIDR refactor would silently re-open the bypass for 64:ff9b:1::/48 or any new translation range. - Add tests for expanded / zero-padded IPv4-compatible IPv6 forms ([0:0:0:0:0:0:7f00:1], fully zero-padded, mixed [0:...:127.0.0.1]). These pin the assumption that the WHATWG URL parser collapses these inputs to ::xxxx[:yyyy]; without them, a future Node anomaly would silently regress the bypass. - Add public IPv6 positive tests (Cloudflare 2606:4700::, Google 2001:4860::). Regression guard against over-blocking. - Add NAT64 + RFC1918 embedded-IP tests (10/8, 172.16/12, 192.168/16) to document SSRF coverage explicitly rather than relying on the prefix check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
@magyargergo Thanks for the review — pushed Source (
Tests (
29/29 unit tests passing locally; |
Security Vulnerability Report
Type: SSRF — IPv6 forms that embed IPv4 addresses bypass the private-address blocklist
Severity: High
Location:
gitnexus/src/server/git-clone.ts—assertNotPrivateIPv6Description
validateGitUrl()is the gate that protects/api/analyzeand thecloneOrPullpath from SSRF when a caller supplies a clone URL. It already blocks IPv4-mapped IPv6 (::ffff:127.0.0.1/::ffff:7f00:1), but two related forms still slip through and remain routable to the embedded IPv4 on common stacks:IPv4-compatible IPv6 (RFC 4291 § 2.5.5.1, deprecated form:
::w.x.y.z).Node's URL parser collapses
http://[::127.0.0.1]/to hostname::7f00:1. There is no::ffff:marker, so the existing:ffff:checks miss it. The Linux kernel and many other stacks still route this to127.0.0.1.NAT64 well-known prefix (RFC 6052:
64:ff9b::/96, plus RFC 8215's local64:ff9b:1::/48).On any host with NAT64 resolution enabled,
64:ff9b::7f00:1translates to127.0.0.1and64:ff9b::a9fe:a9fetranslates to169.254.169.254(cloud metadata).Reproduction
Before this patch, all of the following pass
validateGitUrl:Any caller that can reach
/api/analyze(CORS allows localhost, RFC 1918 LAN, andhttps://gitnexus.vercel.app) could submit one of these URLs, and on any host with the right kernel/NAT64 config the underlyinggit clonewould attempt to connect to the embedded private IPv4. stderr is suppressed, but timing side-channels and redirect-based probes against the clone target remain viable.Fix
Extend
assertNotPrivateIPv6with two additional checks:::xxxxor::xxxx:yyyy— the only legitimate addresses in that range (::1,::) are already blocked by the loopback/unspecified checks above, and every remaining value in that compressed range is either IPv4-compatible IPv6 (deprecated, embeds an IPv4 in the last 32 bits) or extremely low-numbered IPv6 that no public host should be hosted on.64:ff9b:(covers RFC 6052 well-known and RFC 8215 local NAT64 prefixes).Tests
Added cases in
gitnexus/test/unit/git-clone.test.tsfor IPv4-compatible IPv6 (::127.0.0.1,::7f00:1,::a9fe:a9fe) and NAT64 (64:ff9b::7f00:1,64:ff9b::a9fe:a9fe,64:ff9b:1::1). Verified manually that legitimate public IPv6 (e.g.2606:4700:4700::1111,2001:db8::1) still passes.Impact
Without this fix, a caller able to submit clone URLs to
/api/analyze(or usecloneOrPulldirectly) can directgit cloneat loopback (127.0.0.1) or link-local cloud metadata (169.254.169.254) via IPv6 forms. Combined with the recently fixed::ffff:IPv4-mapped form, this closes the IPv6-side bypass surface to known-routable embedded-IPv4 representations.Found by Aeon — automated security scanner