(ip: string, denyList: string[])
| 55 | * @throws Error if IP is in deny list |
| 56 | */ |
| 57 | export function isDeniedIP(ip: string, denyList: string[]): void { |
| 58 | let parsedIp = ipaddr.parse(ip) |
| 59 | |
| 60 | // Normalize IPv4-mapped IPv6 addresses to IPv4 before checking |
| 61 | // This prevents bypass of IPv4 deny list rules via ::ffff:x.x.x.x addresses |
| 62 | if (parsedIp.kind() === 'ipv6') { |
| 63 | const ipv6Addr = parsedIp as ipaddr.IPv6 |
| 64 | if (ipv6Addr.isIPv4MappedAddress()) { |
| 65 | parsedIp = ipv6Addr.toIPv4Address() |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | for (const entry of denyList) { |
| 70 | if (entry.includes('/')) { |
| 71 | try { |
| 72 | const [rangeAddr, mask] = ipaddr.parseCIDR(entry) |
| 73 | let parsedRange = rangeAddr |
| 74 | let adjustedMask = mask |
| 75 | |
| 76 | // Also normalize deny list entries |
| 77 | if (parsedRange.kind() === 'ipv6' && (parsedRange as ipaddr.IPv6).isIPv4MappedAddress()) { |
| 78 | if (mask < 96) continue // malformed IPv4-mapped CIDR — skip |
| 79 | parsedRange = (parsedRange as ipaddr.IPv6).toIPv4Address() |
| 80 | adjustedMask -= 96 |
| 81 | } |
| 82 | |
| 83 | if (parsedIp.kind() === parsedRange.kind()) { |
| 84 | if (parsedIp.match(parsedRange, adjustedMask)) { |
| 85 | throw new Error('Access to this host is denied by policy.') |
| 86 | } |
| 87 | } |
| 88 | } catch (error) { |
| 89 | throw new Error(`isDeniedIP: ${error}`) |
| 90 | } |
| 91 | } else { |
| 92 | // Try to parse and normalize the deny list entry for consistent comparison |
| 93 | // This handles non-canonical IPv6 addresses (e.g., FE80::1, 2001:0DB8::1) |
| 94 | if (ipaddr.isValid(entry)) { |
| 95 | let parsedEntry = ipaddr.parse(entry) |
| 96 | |
| 97 | // Normalize IPv4-mapped IPv6 entries |
| 98 | if (parsedEntry.kind() === 'ipv6' && (parsedEntry as ipaddr.IPv6).isIPv4MappedAddress()) { |
| 99 | parsedEntry = (parsedEntry as ipaddr.IPv6).toIPv4Address() |
| 100 | } |
| 101 | |
| 102 | // Compare normalized forms |
| 103 | if (parsedIp.toString() === parsedEntry.toString()) { |
| 104 | throw new Error('Access to this host is denied by policy.') |
| 105 | } |
| 106 | } else { |
| 107 | // Not a valid IP - compare as-is (e.g., hostname like "localhost") |
| 108 | if (parsedIp.toString() === entry) { |
| 109 | throw new Error('Access to this host is denied by policy.') |
| 110 | } |
| 111 | } |
| 112 | } |
| 113 | } |
| 114 | } |
no test coverage detected