mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-29 01:41:42 +00:00
fix(security): block IPv4-compatible IPv6 and NAT64 SSRF bypasses
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.
This commit is contained in:
parent
46586a8319
commit
bcd3cc198a
2 changed files with 36 additions and 0 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue