11import type { NextApiRequest , NextApiResponse } from 'next' ;
2+ import { isIP } from 'node:net' ;
23
34import logger from '@/lib/logger' ;
45import { safeStringify } from '@/utils/safeStringify' ;
@@ -8,20 +9,140 @@ const UPSTREAM_FETCH_TIMEOUT_MS = 5000;
89const CACHE_CONTROL =
910 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=604800' ;
1011
11- const PRIVATE_HOST_PATTERNS = [
12- / ^ l o c a l h o s t $ / i,
13- / ^ 1 2 7 \. / ,
14- / ^ 1 0 \. / ,
15- / ^ 1 9 2 \. 1 6 8 \. / ,
16- / ^ 1 7 2 \. ( 1 [ 6 - 9 ] | 2 \d | 3 [ 0 - 1 ] ) \. / ,
17- / ^ 1 6 9 \. 2 5 4 \. / ,
18- / ^ 0 \. / ,
19- / ^ \[ ? : : 1 \] ? $ / i,
12+ const IPV4_BLOCKED_RANGES : Array < [ number , number ] > = [
13+ [ 0x00000000 , 8 ] , // 0.0.0.0/8
14+ [ 0x0a000000 , 8 ] , // 10.0.0.0/8
15+ [ 0x64400000 , 10 ] , // 100.64.0.0/10
16+ [ 0x7f000000 , 8 ] , // 127.0.0.0/8
17+ [ 0xa9fe0000 , 16 ] , // 169.254.0.0/16
18+ [ 0xac100000 , 12 ] , // 172.16.0.0/12
19+ [ 0xc0000000 , 24 ] , // 192.0.0.0/24
20+ [ 0xc0000200 , 24 ] , // 192.0.2.0/24
21+ [ 0xc0a80000 , 16 ] , // 192.168.0.0/16
22+ [ 0xc6120000 , 15 ] , // 198.18.0.0/15
23+ [ 0xc6336400 , 24 ] , // 198.51.100.0/24
24+ [ 0xcb007100 , 24 ] , // 203.0.113.0/24
25+ [ 0xe0000000 , 4 ] , // 224.0.0.0/4
26+ [ 0xf0000000 , 4 ] , // 240.0.0.0/4
2027] ;
2128
22- const isPrivateHost = ( hostname : string ) =>
23- hostname . toLowerCase ( ) . endsWith ( '.localhost' ) ||
24- PRIVATE_HOST_PATTERNS . some ( ( pattern ) => pattern . test ( hostname ) ) ;
29+ const IPV6_BLOCKED_RANGES : Array < [ bigint , number ] > = [
30+ [ 0n , 128 ] , // ::/128
31+ [ 1n , 128 ] , // ::1/128
32+ [ 0x0100n << 112n , 64 ] , // 100::/64
33+ [ 0x20010db8n << 96n , 32 ] , // 2001:db8::/32
34+ [ 0xfc00n << 112n , 7 ] , // fc00::/7
35+ [ 0xfe80n << 112n , 10 ] , // fe80::/10
36+ [ 0xff00n << 112n , 8 ] , // ff00::/8
37+ ] ;
38+
39+ const stripIpv6Brackets = ( hostname : string ) =>
40+ hostname . startsWith ( '[' ) && hostname . endsWith ( ']' )
41+ ? hostname . slice ( 1 , - 1 )
42+ : hostname ;
43+
44+ const parseIpv4 = ( address : string ) => {
45+ const parts = address . split ( '.' ) ;
46+ if ( parts . length !== 4 ) return null ;
47+
48+ return parts . reduce < number | null > ( ( result , part ) => {
49+ const octet = Number ( part ) ;
50+ if (
51+ result === null ||
52+ ! Number . isInteger ( octet ) ||
53+ octet < 0 ||
54+ octet > 255
55+ ) {
56+ return null ;
57+ }
58+
59+ return ( ( result << 8 ) + octet ) >>> 0 ;
60+ } , 0 ) ;
61+ } ;
62+
63+ const parseIpv6 = ( address : string ) => {
64+ const compressedParts = address . split ( '::' ) ;
65+ if ( compressedParts . length > 2 ) return null ;
66+
67+ const [ head = '' , tail = '' ] = compressedParts ;
68+ const headParts = head ? head . split ( ':' ) : [ ] ;
69+ const tailParts = tail ? tail . split ( ':' ) : [ ] ;
70+ const missingParts = 8 - headParts . length - tailParts . length ;
71+
72+ if ( missingParts < 0 || ( ! address . includes ( '::' ) && missingParts !== 0 ) ) {
73+ return null ;
74+ }
75+
76+ const parts = [
77+ ...headParts ,
78+ ...Array < string > ( missingParts ) . fill ( '0' ) ,
79+ ...tailParts ,
80+ ] ;
81+
82+ return parts . reduce < bigint | null > ( ( result , part ) => {
83+ if ( result === null || ! / ^ [ 0 - 9 a - f ] { 1 , 4 } $ / i. test ( part ) ) {
84+ return null ;
85+ }
86+
87+ return ( result << 16n ) + BigInt ( `0x${ part } ` ) ;
88+ } , 0n ) ;
89+ } ;
90+
91+ const isIpv4InRange = ( address : number , [ range , prefix ] : [ number , number ] ) => {
92+ const mask = ( 0xffffffff << ( 32 - prefix ) ) >>> 0 ;
93+ return ( address & mask ) === ( range & mask ) ;
94+ } ;
95+
96+ const isIpv6InRange = ( address : bigint , [ range , prefix ] : [ bigint , number ] ) => {
97+ const mask = ( ( 1n << BigInt ( prefix ) ) - 1n ) << BigInt ( 128 - prefix ) ;
98+ return ( address & mask ) === ( range & mask ) ;
99+ } ;
100+
101+ const getIpv4FromMappedIpv6 = ( address : bigint ) => {
102+ const ipv4MappedPrefix = 0xffffn ;
103+ if ( address >> 32n !== ipv4MappedPrefix ) return null ;
104+
105+ return Number ( address & 0xffffffffn ) ;
106+ } ;
107+
108+ const isPrivateIp = ( hostname : string ) => {
109+ const address = stripIpv6Brackets ( hostname ) ;
110+ const ipVersion = isIP ( address ) ;
111+
112+ if ( ipVersion === 4 ) {
113+ const ipv4 = parseIpv4 ( address ) ;
114+ return (
115+ ipv4 !== null &&
116+ IPV4_BLOCKED_RANGES . some ( ( range ) => isIpv4InRange ( ipv4 , range ) )
117+ ) ;
118+ }
119+
120+ if ( ipVersion === 6 ) {
121+ const ipv6 = parseIpv6 ( address ) ;
122+ if ( ipv6 === null ) return false ;
123+
124+ const mappedIpv4 = getIpv4FromMappedIpv6 ( ipv6 ) ;
125+ if (
126+ mappedIpv4 !== null &&
127+ IPV4_BLOCKED_RANGES . some ( ( range ) => isIpv4InRange ( mappedIpv4 , range ) )
128+ ) {
129+ return true ;
130+ }
131+
132+ return IPV6_BLOCKED_RANGES . some ( ( range ) => isIpv6InRange ( ipv6 , range ) ) ;
133+ }
134+
135+ return false ;
136+ } ;
137+
138+ const isPrivateHost = ( hostname : string ) => {
139+ const normalizedHostname = hostname . toLowerCase ( ) ;
140+ return (
141+ normalizedHostname === 'localhost' ||
142+ normalizedHostname . endsWith ( '.localhost' ) ||
143+ isPrivateIp ( hostname )
144+ ) ;
145+ } ;
25146
26147export default async function handler (
27148 req : NextApiRequest ,
0 commit comments