(address: string)
| 86 | } |
| 87 | |
| 88 | function isBlockedV6(address: string): boolean { |
| 89 | const lower = address.toLowerCase() |
| 90 | |
| 91 | // ::1 loopback explicitly allowed |
| 92 | if (lower === '::1') return false |
| 93 | |
| 94 | // :: unspecified |
| 95 | if (lower === '::') return true |
| 96 | |
| 97 | // IPv4-mapped IPv6 (0:0:0:0:0:ffff:X:Y in any representation — ::ffff:a.b.c.d, |
| 98 | // ::ffff:XXXX:YYYY, expanded, or partially expanded). Extract the embedded |
| 99 | // IPv4 address and delegate to the v4 check. Without this, hex-form mapped |
| 100 | // addresses (e.g. ::ffff:a9fe:a9fe = 169.254.169.254) bypass the guard. |
| 101 | const mappedV4 = extractMappedIPv4(lower) |
| 102 | if (mappedV4 !== null) { |
| 103 | return isBlockedV4(mappedV4) |
| 104 | } |
| 105 | |
| 106 | // fc00::/7 — unique local addresses (fc00:: through fdff::) |
| 107 | if (lower.startsWith('fc') || lower.startsWith('fd')) { |
| 108 | return true |
| 109 | } |
| 110 | |
| 111 | // fe80::/10 — link-local. The /10 means fe80 through febf, but the first |
| 112 | // hextet is always fe80 in practice (RFC 4291 requires the next 54 bits |
| 113 | // to be zero). Check both to be safe. |
| 114 | const firstHextet = lower.split(':')[0] |
| 115 | if ( |
| 116 | firstHextet && |
| 117 | firstHextet.length === 4 && |
| 118 | firstHextet >= 'fe80' && |
| 119 | firstHextet <= 'febf' |
| 120 | ) { |
| 121 | return true |
| 122 | } |
| 123 | |
| 124 | return false |
| 125 | } |
| 126 | |
| 127 | /** |
| 128 | * Expand `::` and optional trailing dotted-decimal so an IPv6 address is |
no test coverage detected