validate and canonicalize outbound request hosts
This commit is contained in:
2 files changed
+134
-26
No files matched your search
@@ -73,6 +73,55 @@ describe('ipCategory — fc/fd anchoring', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('ipCategory — IPv6 transition address bypass (NAT64 / 6to4 / Teredo)', () => {
|
||||
// new URL() normalizes/compresses a bracketed v6 host; these are the forms it emits.
|
||||
const norm = (v6: string) => new URL(`http://[${v6}]`).hostname.replace(/^\[|\]$/g, '');
|
||||
|
||||
test('NAT64 WKP (64:ff9b::/96) embedded loopback/metadata/private is caught', () => {
|
||||
expect(ipCategory(norm('64:ff9b::7f00:1'))).toBe('loopback'); // 127.0.0.1
|
||||
expect(ipCategory(norm('64:ff9b::a9fe:a9fe'))).toBe('metadata'); // 169.254.169.254
|
||||
expect(ipCategory(norm('64:ff9b::a00:1'))).toBe('private'); // 10.0.0.1
|
||||
});
|
||||
|
||||
test('NAT64 local prefix (64:ff9b:1::/48) is blocked wholesale (reserved)', () => {
|
||||
// The /48 embeds its v4 around the u-byte, not the last 32 bits, so the whole
|
||||
// prefix is blocked outright rather than decoded - it has no legitimate use.
|
||||
expect(ipCategory(norm('64:ff9b:1::7f00:1'))).toBe('reserved');
|
||||
expect(ipCategory(norm('64:ff9b:1::a9fe:a9fe'))).toBe('reserved');
|
||||
expect(ipCategory(norm('64:ff9b:1:a00:0:100::'))).toBe('reserved'); // RFC-6052 /48 layout
|
||||
});
|
||||
|
||||
test('ISATAP interface-id (::0:5efe:v4 / ::200:5efe:v4, any prefix) is caught', () => {
|
||||
expect(ipCategory(norm('::0:5efe:127.0.0.1'))).toBe('loopback');
|
||||
expect(ipCategory(norm('::200:5efe:169.254.169.254'))).toBe('metadata');
|
||||
expect(ipCategory(norm('::5efe:10.0.0.1'))).toBe('private');
|
||||
// ISATAP is defined for any /64 prefix, including a global one.
|
||||
expect(ipCategory(norm('2001:db8::5efe:10.0.0.1'))).toBe('private');
|
||||
});
|
||||
|
||||
test('6to4 (2002::/16) embedded v4 is caught', () => {
|
||||
expect(ipCategory(norm('2002:7f00:1::1'))).toBe('loopback'); // 127.0.0.1
|
||||
expect(ipCategory(norm('2002:a9fe:a9fe::1'))).toBe('metadata'); // 169.254.169.254
|
||||
expect(ipCategory(norm('2002:c0a8:1::1'))).toBe('private'); // 192.168.0.1
|
||||
});
|
||||
|
||||
test('Teredo (2001:0000::/32) inverted client v4 is caught', () => {
|
||||
// last two hextets are the client v4, bitwise-inverted: ~0x80ff.~0xfffe = 127.0.0.1
|
||||
expect(ipCategory(norm('2001:0:ffff:ffff:ffff:ffff:80ff:fffe'))).toBe('loopback');
|
||||
});
|
||||
|
||||
test('a transition prefix wrapping a PUBLIC v4 is NOT over-blocked', () => {
|
||||
expect(ipCategory(norm('64:ff9b::8.8.8.8'))).toBe(null); // NAT64 WKP of 8.8.8.8
|
||||
expect(ipCategory(norm('2002:0808:0808::1'))).toBe(null); // 6to4 of 8.8.8.8
|
||||
expect(ipCategory(norm('::0:5efe:8.8.8.8'))).toBe(null); // ISATAP of 8.8.8.8
|
||||
});
|
||||
|
||||
test('a genuine public IPv6 address is not misread as a transition address', () => {
|
||||
expect(ipCategory(norm('2606:4700:4700::1111'))).toBe(null); // cloudflare
|
||||
expect(ipCategory(norm('2001:4860:4860::8888'))).toBe(null); // google (2001 but not Teredo/ISATAP)
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateRepositoryForSave — backup destination SSRF guard', () => {
|
||||
test('blocks loopback and cloud-metadata repo hosts', () => {
|
||||
expect(validateRepositoryForSave('rest:http://169.254.169.254/repo')).not.toBeNull();
|
||||
|
||||
Reference in new issue
Block a user