diff --git a/gitnexus/src/server/git-clone.ts b/gitnexus/src/server/git-clone.ts index 0f7bc2653..53b0ea2e6 100644 --- a/gitnexus/src/server/git-clone.ts +++ b/gitnexus/src/server/git-clone.ts @@ -139,6 +139,23 @@ function assertNotPrivateIPv6(ip: string): void { if (lower.includes(':ffff:')) { throw new Error('Cloning from private/internal addresses is not allowed'); } + + // 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 "::7f00:1" — the IPv4 + // is hidden in the last 32 bits without the ::ffff: marker, so the check + // above misses it. The form is still routable to the embedded IPv4 on most + // network stacks, so any address compressed to ::xxxx[:yyyy] must be blocked. + if (/^::[0-9a-f]{1,4}(:[0-9a-f]{1,4})?$/.test(lower)) { + throw new Error('Cloning from private/internal addresses is not allowed'); + } + + // NAT64 well-known prefix (RFC 6052 § 2.1: 64:ff9b::/96, plus the local + // 64:ff9b:1::/48 from RFC 8215). Maps any IPv4 address — including private + // ranges — into IPv6, so a host with NAT64 can reach the embedded IPv4 via + // e.g. 64:ff9b::7f00:1 → 127.0.0.1. + if (lower.startsWith('64:ff9b:')) { + throw new Error('Cloning from private/internal addresses is not allowed'); + } } function assertNotPrivateIPv4(ip: string): void { diff --git a/gitnexus/test/unit/git-clone.test.ts b/gitnexus/test/unit/git-clone.test.ts index 832f95641..6364cd372 100644 --- a/gitnexus/test/unit/git-clone.test.ts +++ b/gitnexus/test/unit/git-clone.test.ts @@ -96,6 +96,25 @@ describe('git-clone', () => { ); }); + it('blocks IPv4-compatible IPv6 (RFC 4291 deprecated, ::w.x.y.z)', () => { + // Node's URL parser collapses ::127.0.0.1 to ::7f00:1 — no ::ffff: marker, + // but still routable to 127.0.0.1 on most stacks. + expect(() => validateGitUrl('http://[::127.0.0.1]/repo.git')).toThrow('private/internal'); + expect(() => validateGitUrl('http://[::7f00:1]/repo.git')).toThrow('private/internal'); + // 169.254.169.254 (cloud metadata) embedded as IPv4-compatible + expect(() => validateGitUrl('http://[::a9fe:a9fe]/repo.git')).toThrow('private/internal'); + }); + + it('blocks NAT64 well-known prefix (64:ff9b::/96)', () => { + // 64:ff9b::7f00:1 → 127.0.0.1 via NAT64 translation + expect(() => validateGitUrl('http://[64:ff9b::7f00:1]/repo.git')).toThrow('private/internal'); + expect(() => validateGitUrl('http://[64:ff9b::a9fe:a9fe]/repo.git')).toThrow( + 'private/internal', + ); + // RFC 8215 local NAT64 prefix + expect(() => validateGitUrl('http://[64:ff9b:1::1]/repo.git')).toThrow('private/internal'); + }); + it('does not block valid public IPs', () => { expect(() => validateGitUrl('https://140.82.121.4/repo.git')).not.toThrow(); });