mirror of
https://github.com/documenso/documenso.git
synced 2026-09-29 08:14:31 +10:00
@@ -81,13 +81,20 @@ describe('isPrivateUrl', () => {
|
||||
expect(isPrivateUrl('http://[fd12::1]')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not catch IPv4-mapped IPv6 in URL form (URL parser normalizes to hex)', () => {
|
||||
// new URL() normalizes "::ffff:127.0.0.1" to "::ffff:7f00:1" which none
|
||||
// of the checks handle. This is fine because dns.lookup never returns
|
||||
// IPv4-mapped addresses — it returns plain IPv4 (family: 4) instead.
|
||||
expect(isPrivateUrl('http://[::ffff:127.0.0.1]')).toBe(false);
|
||||
expect(isPrivateUrl('http://[::ffff:10.0.0.1]')).toBe(false);
|
||||
it('should detect private IPv4-mapped IPv6 addresses (URL parser normalizes to hex)', () => {
|
||||
// new URL() normalizes "::ffff:127.0.0.1" to the hex form "::ffff:7f00:1",
|
||||
// so the embedded IPv4 must be decoded and re-checked. Otherwise a literal
|
||||
// host such as http://[::ffff:127.0.0.1] bypasses every dotted-decimal
|
||||
// check above (SSRF, see #2901).
|
||||
expect(isPrivateUrl('http://[::ffff:127.0.0.1]')).toBe(true);
|
||||
expect(isPrivateUrl('http://[::ffff:10.0.0.1]')).toBe(true);
|
||||
expect(isPrivateUrl('http://[::ffff:192.168.0.1]')).toBe(true);
|
||||
expect(isPrivateUrl('http://[::ffff:169.254.169.254]')).toBe(true);
|
||||
});
|
||||
|
||||
it('should still allow public IPv4-mapped IPv6 addresses', () => {
|
||||
expect(isPrivateUrl('http://[::ffff:8.8.8.8]')).toBe(false);
|
||||
expect(isPrivateUrl('http://[::ffff:1.1.1.1]')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -69,11 +69,25 @@ export const isPrivateUrl = (url: string): boolean => {
|
||||
}
|
||||
}
|
||||
|
||||
// IPv4-mapped IPv6 (e.g. ::ffff:127.0.0.1)
|
||||
const v4Mapped = normalizedHost.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
|
||||
// IPv4-mapped IPv6, dotted form (e.g. ::ffff:127.0.0.1)
|
||||
const v4MappedDotted = normalizedHost.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/i);
|
||||
|
||||
if (v4Mapped) {
|
||||
return isPrivateUrl(`http://${v4Mapped[1]}`);
|
||||
if (v4MappedDotted) {
|
||||
return isPrivateUrl(`http://${v4MappedDotted[1]}`);
|
||||
}
|
||||
|
||||
// IPv4-mapped IPv6, hex form (e.g. ::ffff:7f00:1). `new URL()` normalizes the
|
||||
// dotted form above to this, so it must be decoded to the embedded IPv4 as
|
||||
// well - otherwise a literal host such as `http://[::ffff:127.0.0.1]` slips
|
||||
// through every dotted-decimal check above (SSRF, see #2901).
|
||||
const v4MappedHex = normalizedHost.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
|
||||
|
||||
if (v4MappedHex) {
|
||||
const high = parseInt(v4MappedHex[1], 16);
|
||||
const low = parseInt(v4MappedHex[2], 16);
|
||||
const ipv4 = [high >> 8, high & 0xff, low >> 8, low & 0xff].join('.');
|
||||
|
||||
return isPrivateUrl(`http://${ipv4}`);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user