* Reject disallowed IPv6 literals: loopback (`::1`), unspecified (`::`), * IPv4-mapped (`::ffff:…`), link-local (`fe80::/10`), and unique-local * (`fc00::/7`). `inner` is the bracket-stripped, lowercased host. * * The IPv4-mapped class is rejected wholesale rather than decoding the * embedded v
(inner: string)
| 111 | * decode. This closes the `http://[::ffff:127.0.0.1]` SSRF-guard bypass. |
| 112 | */ |
| 113 | function assertNotLocalIpv6(inner: string): void { |
| 114 | // Loopback (::1) and unspecified (::). |
| 115 | if (inner === '::1' || inner === '::') { |
| 116 | throw localTargetError('target-url', 'localhost targets are not allowed', LOCAL_DEV_HINT); |
| 117 | } |
| 118 | // IPv4-mapped IPv6 (`::ffff:a.b.c.d`, normalized to `::ffff:hhhh:hhhh`). |
| 119 | if (inner.startsWith('::ffff:')) { |
| 120 | throw localTargetError( |
| 121 | 'target-url', |
| 122 | 'IPv4-mapped IPv6 addresses are not allowed; use the IPv4 form or a hostname', |
| 123 | LOCAL_DEV_HINT, |
| 124 | ); |
| 125 | } |
| 126 | // Link-local fe80::/10 (first hextet fe80–febf). |
| 127 | if (/^fe[89ab][0-9a-f]:/.test(inner)) { |
| 128 | throw localTargetError('target-url', 'link-local addresses are not allowed', LOCAL_DEV_HINT); |
| 129 | } |
| 130 | // Unique-local fc00::/7 (first hextet fc00–fdff). |
| 131 | if (/^f[cd][0-9a-f]{2}:/.test(inner)) { |
| 132 | throw localTargetError( |
| 133 | 'target-url', |
| 134 | 'unique-local (private) addresses are not allowed', |
| 135 | LOCAL_DEV_HINT, |
| 136 | ); |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | function localTargetError(field: string, reason: string, hint?: string): ApiError { |
| 141 | return ApiError.fromEnvelope({ |
no test coverage detected