Skip to content

Commit 0da88ff

Browse files
committed
added ipv6 and addiitonal block ranges
1 parent b94002d commit 0da88ff

1 file changed

Lines changed: 133 additions & 12 deletions

File tree

src/pages/api/token-icon.ts

Lines changed: 133 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { NextApiRequest, NextApiResponse } from 'next';
2+
import { isIP } from 'node:net';
23

34
import logger from '@/lib/logger';
45
import { safeStringify } from '@/utils/safeStringify';
@@ -8,20 +9,140 @@ const UPSTREAM_FETCH_TIMEOUT_MS = 5000;
89
const CACHE_CONTROL =
910
'public, max-age=86400, s-maxage=86400, stale-while-revalidate=604800';
1011

11-
const PRIVATE_HOST_PATTERNS = [
12-
/^localhost$/i,
13-
/^127\./,
14-
/^10\./,
15-
/^192\.168\./,
16-
/^172\.(1[6-9]|2\d|3[0-1])\./,
17-
/^169\.254\./,
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-9a-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

26147
export default async function handler(
27148
req: NextApiRequest,

0 commit comments

Comments
 (0)