@actuate-media/cms-core 0.11.1 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/api/cron-routes.test.d.ts +2 -0
- package/dist/__tests__/api/cron-routes.test.d.ts.map +1 -0
- package/dist/__tests__/api/cron-routes.test.js +67 -0
- package/dist/__tests__/api/cron-routes.test.js.map +1 -0
- package/dist/__tests__/auth/password.test.js +82 -3
- package/dist/__tests__/auth/password.test.js.map +1 -1
- package/dist/__tests__/auth/session.test.js +54 -1
- package/dist/__tests__/auth/session.test.js.map +1 -1
- package/dist/__tests__/cron/cron.test.d.ts +2 -0
- package/dist/__tests__/cron/cron.test.d.ts.map +1 -0
- package/dist/__tests__/cron/cron.test.js +262 -0
- package/dist/__tests__/cron/cron.test.js.map +1 -0
- package/dist/__tests__/security/encrypted-fields.test.d.ts +2 -0
- package/dist/__tests__/security/encrypted-fields.test.d.ts.map +1 -0
- package/dist/__tests__/security/encrypted-fields.test.js +60 -0
- package/dist/__tests__/security/encrypted-fields.test.js.map +1 -0
- package/dist/__tests__/security/safe-fetch.test.d.ts +2 -0
- package/dist/__tests__/security/safe-fetch.test.d.ts.map +1 -0
- package/dist/__tests__/security/safe-fetch.test.js +97 -0
- package/dist/__tests__/security/safe-fetch.test.js.map +1 -0
- package/dist/__tests__/security/ssrf.test.d.ts +2 -0
- package/dist/__tests__/security/ssrf.test.d.ts.map +1 -0
- package/dist/__tests__/security/ssrf.test.js +209 -0
- package/dist/__tests__/security/ssrf.test.js.map +1 -0
- package/dist/api/handler-factory.d.ts.map +1 -1
- package/dist/api/handler-factory.js +3 -0
- package/dist/api/handler-factory.js.map +1 -1
- package/dist/api/handlers.d.ts.map +1 -1
- package/dist/api/handlers.js +84 -1
- package/dist/api/handlers.js.map +1 -1
- package/dist/auth/oauth.d.ts +8 -0
- package/dist/auth/oauth.d.ts.map +1 -1
- package/dist/auth/oauth.js +39 -1
- package/dist/auth/oauth.js.map +1 -1
- package/dist/auth/password.d.ts +35 -2
- package/dist/auth/password.d.ts.map +1 -1
- package/dist/auth/password.js +97 -7
- package/dist/auth/password.js.map +1 -1
- package/dist/auth/session.d.ts +9 -0
- package/dist/auth/session.d.ts.map +1 -1
- package/dist/auth/session.js +54 -1
- package/dist/auth/session.js.map +1 -1
- package/dist/cron/index.d.ts +72 -0
- package/dist/cron/index.d.ts.map +1 -0
- package/dist/cron/index.js +222 -0
- package/dist/cron/index.js.map +1 -0
- package/dist/security/encrypted-fields.d.ts +9 -0
- package/dist/security/encrypted-fields.d.ts.map +1 -1
- package/dist/security/encrypted-fields.js +52 -1
- package/dist/security/encrypted-fields.js.map +1 -1
- package/dist/security/ip-canon.d.ts +71 -0
- package/dist/security/ip-canon.d.ts.map +1 -0
- package/dist/security/ip-canon.js +352 -0
- package/dist/security/ip-canon.js.map +1 -0
- package/dist/security/rate-limit.d.ts +0 -4
- package/dist/security/rate-limit.d.ts.map +1 -1
- package/dist/security/rate-limit.js +30 -0
- package/dist/security/rate-limit.js.map +1 -1
- package/dist/security/safe-fetch.d.ts +30 -8
- package/dist/security/safe-fetch.d.ts.map +1 -1
- package/dist/security/safe-fetch.js +32 -6
- package/dist/security/safe-fetch.js.map +1 -1
- package/dist/security/webhook.d.ts +20 -2
- package/dist/security/webhook.d.ts.map +1 -1
- package/dist/security/webhook.js +100 -30
- package/dist/security/webhook.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IP canonicalization for SSRF defense.
|
|
3
|
+
*
|
|
4
|
+
* The previous regex-based approach in `webhook.ts` only matched the literal
|
|
5
|
+
* dotted-quad form. It missed every encoding that `getaddrinfo` (and therefore
|
|
6
|
+
* `fetch`) accepts:
|
|
7
|
+
*
|
|
8
|
+
* - Decimal: http://2130706433/ → 127.0.0.1
|
|
9
|
+
* - Octal: http://0177.0.0.1/ → 127.0.0.1
|
|
10
|
+
* - Hex: http://0x7f.0.0.1/ → 127.0.0.1
|
|
11
|
+
* - Short-form: http://127.1/ → 127.0.0.1
|
|
12
|
+
* - IPv4-mapped v6: http://[::ffff:127.0.0.1] → 127.0.0.1
|
|
13
|
+
* - IPv4-compat v6: http://[::127.0.0.1] → 127.0.0.1
|
|
14
|
+
*
|
|
15
|
+
* This module canonicalizes any of those forms to a 4-octet IPv4 string (or
|
|
16
|
+
* a normalized 16-byte IPv6 address), then range-checks against a complete
|
|
17
|
+
* private/internal block list including AWS/GCP/Azure metadata endpoints and
|
|
18
|
+
* the carrier-grade NAT range.
|
|
19
|
+
*/
|
|
20
|
+
const PRIVATE_IPV4_RANGES = [
|
|
21
|
+
// 0.0.0.0/8 "this network"
|
|
22
|
+
{ network: 0x00_00_00_00, mask: 0xff_00_00_00, reason: '0.0.0.0/8 (this network)' },
|
|
23
|
+
// 10.0.0.0/8 RFC1918 private
|
|
24
|
+
{ network: 0x0a_00_00_00, mask: 0xff_00_00_00, reason: '10.0.0.0/8 (RFC1918 private)' },
|
|
25
|
+
// 100.64.0.0/10 RFC6598 carrier-grade NAT
|
|
26
|
+
{ network: 0x64_40_00_00, mask: 0xff_c0_00_00, reason: '100.64.0.0/10 (CGNAT)' },
|
|
27
|
+
// 127.0.0.0/8 loopback
|
|
28
|
+
{ network: 0x7f_00_00_00, mask: 0xff_00_00_00, reason: '127.0.0.0/8 (loopback)' },
|
|
29
|
+
// 169.254.0.0/16 link-local + cloud metadata (AWS 169.254.169.254 is in here)
|
|
30
|
+
{ network: 0xa9_fe_00_00, mask: 0xff_ff_00_00, reason: '169.254.0.0/16 (link-local + metadata)' },
|
|
31
|
+
// 172.16.0.0/12 RFC1918 private
|
|
32
|
+
{ network: 0xac_10_00_00, mask: 0xff_f0_00_00, reason: '172.16.0.0/12 (RFC1918 private)' },
|
|
33
|
+
// 192.0.0.0/24 RFC6890 IETF protocol assignments
|
|
34
|
+
{ network: 0xc0_00_00_00, mask: 0xff_ff_ff_00, reason: '192.0.0.0/24 (IETF reserved)' },
|
|
35
|
+
// 192.0.2.0/24 TEST-NET-1 (documentation)
|
|
36
|
+
{ network: 0xc0_00_02_00, mask: 0xff_ff_ff_00, reason: '192.0.2.0/24 (TEST-NET-1)' },
|
|
37
|
+
// 192.168.0.0/16 RFC1918 private
|
|
38
|
+
{ network: 0xc0_a8_00_00, mask: 0xff_ff_00_00, reason: '192.168.0.0/16 (RFC1918 private)' },
|
|
39
|
+
// 198.18.0.0/15 benchmark
|
|
40
|
+
{ network: 0xc6_12_00_00, mask: 0xff_fe_00_00, reason: '198.18.0.0/15 (benchmark)' },
|
|
41
|
+
// 198.51.100.0/24 TEST-NET-2
|
|
42
|
+
{ network: 0xc6_33_64_00, mask: 0xff_ff_ff_00, reason: '198.51.100.0/24 (TEST-NET-2)' },
|
|
43
|
+
// 203.0.113.0/24 TEST-NET-3
|
|
44
|
+
{ network: 0xcb_00_71_00, mask: 0xff_ff_ff_00, reason: '203.0.113.0/24 (TEST-NET-3)' },
|
|
45
|
+
// 224.0.0.0/4 multicast
|
|
46
|
+
{ network: 0xe0_00_00_00, mask: 0xf0_00_00_00, reason: '224.0.0.0/4 (multicast)' },
|
|
47
|
+
// 240.0.0.0/4 reserved (includes 255.255.255.255 broadcast)
|
|
48
|
+
{ network: 0xf0_00_00_00, mask: 0xf0_00_00_00, reason: '240.0.0.0/4 (reserved)' },
|
|
49
|
+
];
|
|
50
|
+
/**
|
|
51
|
+
* Canonicalize a URL hostname into either a normalized IP address or a
|
|
52
|
+
* passthrough hostname. Handles every IPv4 encoding accepted by `inet_aton`
|
|
53
|
+
* (decimal, octal, hex, short-form), IPv4-mapped IPv6, and bracketed IPv6.
|
|
54
|
+
*/
|
|
55
|
+
export function canonicalizeHostname(rawHostname) {
|
|
56
|
+
const raw = rawHostname;
|
|
57
|
+
// Strip surrounding brackets (URLs use `[::1]` for IPv6 hosts).
|
|
58
|
+
let host = raw;
|
|
59
|
+
if (host.startsWith('[') && host.endsWith(']')) {
|
|
60
|
+
host = host.slice(1, -1);
|
|
61
|
+
}
|
|
62
|
+
// Try IPv6 first — only IPv6 contains ':'.
|
|
63
|
+
if (host.includes(':')) {
|
|
64
|
+
const ipv6 = parseIPv6(host);
|
|
65
|
+
if (ipv6) {
|
|
66
|
+
// IPv4-mapped (::ffff:a.b.c.d) and IPv4-compatible (::a.b.c.d) both
|
|
67
|
+
// tunnel an IPv4 address through IPv6 — collapse to the underlying v4
|
|
68
|
+
// so the v4 range check catches them.
|
|
69
|
+
const tunnelled = extractIPv4FromIPv6(ipv6);
|
|
70
|
+
if (tunnelled !== null) {
|
|
71
|
+
return { raw, ipv4: ipv4ToString(tunnelled), isHostname: false, isValidIp: true };
|
|
72
|
+
}
|
|
73
|
+
return { raw, ipv6: formatIPv6(ipv6), isHostname: false, isValidIp: true };
|
|
74
|
+
}
|
|
75
|
+
// Looked like IPv6 (had a colon) but didn't parse. Mark as a non-hostname
|
|
76
|
+
// *and* not a valid IP — SSRF gates fail closed on this shape.
|
|
77
|
+
return { raw, isHostname: false, isValidIp: false };
|
|
78
|
+
}
|
|
79
|
+
// Try IPv4 in any of: decimal, octal, hex, dotted, short-form.
|
|
80
|
+
const ipv4 = parseIPv4Literal(host);
|
|
81
|
+
if (ipv4 !== null) {
|
|
82
|
+
return { raw, ipv4: ipv4ToString(ipv4), isHostname: false, isValidIp: true };
|
|
83
|
+
}
|
|
84
|
+
// Not an IP literal in any form → treat as DNS hostname.
|
|
85
|
+
return { raw, isHostname: true, isValidIp: false };
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Returns the matching private/internal range when the canonicalized host is
|
|
89
|
+
* one, `null` when it's a safe public IP, or a "malformed" reason when the
|
|
90
|
+
* input claims to be an IP literal but didn't parse cleanly.
|
|
91
|
+
*
|
|
92
|
+
* Defense-in-depth: even if a caller forgets to gate on `isValidIp`, this
|
|
93
|
+
* function still fails closed on the malformed shape (`isHostname=false`
|
|
94
|
+
* with no `ipv4`/`ipv6`).
|
|
95
|
+
*/
|
|
96
|
+
export function isPrivateAddress(host) {
|
|
97
|
+
if (host.ipv4) {
|
|
98
|
+
const num = ipv4StringToNumber(host.ipv4);
|
|
99
|
+
if (num === null)
|
|
100
|
+
return { reason: 'invalid IPv4 numeric form' };
|
|
101
|
+
for (const range of PRIVATE_IPV4_RANGES) {
|
|
102
|
+
// `>>> 0` forces the int32 result of `&` back to an unsigned 32-bit
|
|
103
|
+
// value so comparisons with `range.network` (a positive JS number)
|
|
104
|
+
// work correctly even when the high bit is set (172.16.x.x and up).
|
|
105
|
+
if ((num & range.mask) >>> 0 === range.network) {
|
|
106
|
+
return { reason: range.reason };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
if (host.ipv6) {
|
|
112
|
+
const v6 = parseIPv6(host.ipv6);
|
|
113
|
+
if (!v6)
|
|
114
|
+
return { reason: 'invalid IPv6 form' };
|
|
115
|
+
return checkPrivateIPv6(v6);
|
|
116
|
+
}
|
|
117
|
+
// Malformed IP literal (looked like an IP but didn't parse). Fail closed.
|
|
118
|
+
if (!host.isHostname) {
|
|
119
|
+
return { reason: 'malformed IP literal' };
|
|
120
|
+
}
|
|
121
|
+
// DNS hostname → no IP-level check possible without resolution.
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Parse an IPv4 literal in *any* form `inet_aton` accepts:
|
|
126
|
+
* - 4 parts: a.b.c.d (each 0-255)
|
|
127
|
+
* - 3 parts: a.b.c (c is bottom 16 bits)
|
|
128
|
+
* - 2 parts: a.b (b is bottom 24 bits)
|
|
129
|
+
* - 1 part: a (full 32-bit number)
|
|
130
|
+
*
|
|
131
|
+
* Each part may be decimal, octal (leading 0), or hex (leading 0x).
|
|
132
|
+
*/
|
|
133
|
+
function parseIPv4Literal(input) {
|
|
134
|
+
if (input.length === 0)
|
|
135
|
+
return null;
|
|
136
|
+
const parts = input.split('.');
|
|
137
|
+
if (parts.length === 0 || parts.length > 4)
|
|
138
|
+
return null;
|
|
139
|
+
const nums = [];
|
|
140
|
+
for (const p of parts) {
|
|
141
|
+
if (p.length === 0)
|
|
142
|
+
return null;
|
|
143
|
+
const n = parsePartNumber(p);
|
|
144
|
+
if (n === null)
|
|
145
|
+
return null;
|
|
146
|
+
nums.push(n);
|
|
147
|
+
}
|
|
148
|
+
// Build 32-bit value per inet_aton rules.
|
|
149
|
+
switch (nums.length) {
|
|
150
|
+
case 1: {
|
|
151
|
+
const a = nums[0];
|
|
152
|
+
if (a > 0xff_ff_ff_ff)
|
|
153
|
+
return null;
|
|
154
|
+
return a >>> 0;
|
|
155
|
+
}
|
|
156
|
+
case 2: {
|
|
157
|
+
const [a, b] = nums;
|
|
158
|
+
if (a > 0xff || b > 0xff_ff_ff)
|
|
159
|
+
return null;
|
|
160
|
+
return ((a << 24) | b) >>> 0;
|
|
161
|
+
}
|
|
162
|
+
case 3: {
|
|
163
|
+
const [a, b, c] = nums;
|
|
164
|
+
if (a > 0xff || b > 0xff || c > 0xff_ff)
|
|
165
|
+
return null;
|
|
166
|
+
return ((a << 24) | (b << 16) | c) >>> 0;
|
|
167
|
+
}
|
|
168
|
+
case 4: {
|
|
169
|
+
const [a, b, c, d] = nums;
|
|
170
|
+
if (a > 0xff || b > 0xff || c > 0xff || d > 0xff)
|
|
171
|
+
return null;
|
|
172
|
+
return ((a << 24) | (b << 16) | (c << 8) | d) >>> 0;
|
|
173
|
+
}
|
|
174
|
+
default:
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function parsePartNumber(part) {
|
|
179
|
+
// Hex: 0x...
|
|
180
|
+
if (part.startsWith('0x') || part.startsWith('0X')) {
|
|
181
|
+
const hex = part.slice(2);
|
|
182
|
+
if (hex.length === 0 || !/^[0-9a-fA-F]+$/.test(hex))
|
|
183
|
+
return null;
|
|
184
|
+
const n = parseInt(hex, 16);
|
|
185
|
+
return Number.isFinite(n) ? n : null;
|
|
186
|
+
}
|
|
187
|
+
// Octal: starts with 0 followed by digits (but bare "0" is decimal 0).
|
|
188
|
+
if (part.length > 1 && part.startsWith('0')) {
|
|
189
|
+
if (!/^[0-7]+$/.test(part))
|
|
190
|
+
return null;
|
|
191
|
+
const n = parseInt(part, 8);
|
|
192
|
+
return Number.isFinite(n) ? n : null;
|
|
193
|
+
}
|
|
194
|
+
// Decimal.
|
|
195
|
+
if (!/^[0-9]+$/.test(part))
|
|
196
|
+
return null;
|
|
197
|
+
const n = parseInt(part, 10);
|
|
198
|
+
return Number.isFinite(n) ? n : null;
|
|
199
|
+
}
|
|
200
|
+
function ipv4StringToNumber(s) {
|
|
201
|
+
const parts = s.split('.');
|
|
202
|
+
if (parts.length !== 4)
|
|
203
|
+
return null;
|
|
204
|
+
let n = 0;
|
|
205
|
+
for (const p of parts) {
|
|
206
|
+
const x = parseInt(p, 10);
|
|
207
|
+
if (!Number.isFinite(x) || x < 0 || x > 255)
|
|
208
|
+
return null;
|
|
209
|
+
n = (n << 8) | x;
|
|
210
|
+
}
|
|
211
|
+
return n >>> 0;
|
|
212
|
+
}
|
|
213
|
+
function ipv4ToString(n) {
|
|
214
|
+
return [(n >>> 24) & 0xff, (n >>> 16) & 0xff, (n >>> 8) & 0xff, n & 0xff].join('.');
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Parse an IPv6 address into 8 groups of 16-bit numbers. Supports `::`
|
|
218
|
+
* compression and the `::ffff:a.b.c.d` IPv4-mapped trailing form.
|
|
219
|
+
*
|
|
220
|
+
* Reconstruction order with compression:
|
|
221
|
+
* [head hextets] [zero-fill] [tail hextets] [optional v4 suffix as 2 hextets]
|
|
222
|
+
*
|
|
223
|
+
* Without compression: just `[8 hextets, optional last replaced by v4 suffix]`.
|
|
224
|
+
*/
|
|
225
|
+
function parseIPv6(input) {
|
|
226
|
+
if (input.length === 0)
|
|
227
|
+
return null;
|
|
228
|
+
const doubleColon = input.indexOf('::');
|
|
229
|
+
let head;
|
|
230
|
+
let tail;
|
|
231
|
+
if (doubleColon >= 0) {
|
|
232
|
+
if (input.indexOf('::', doubleColon + 1) !== -1)
|
|
233
|
+
return null; // multiple ::
|
|
234
|
+
head = input
|
|
235
|
+
.slice(0, doubleColon)
|
|
236
|
+
.split(':')
|
|
237
|
+
.filter((s) => s.length > 0);
|
|
238
|
+
tail = input
|
|
239
|
+
.slice(doubleColon + 2)
|
|
240
|
+
.split(':')
|
|
241
|
+
.filter((s) => s.length > 0);
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
head = input.split(':');
|
|
245
|
+
tail = [];
|
|
246
|
+
// No compression: must be exactly 8 hextets (or 7 + a trailing IPv4).
|
|
247
|
+
}
|
|
248
|
+
// The trailing group may be a dotted-quad IPv4. It's always at the end of
|
|
249
|
+
// either `tail` (when compression is present) or `head` (when not).
|
|
250
|
+
let v4Suffix = null;
|
|
251
|
+
const trailingGroupArr = tail.length > 0 ? tail : head;
|
|
252
|
+
const trailingGroup = trailingGroupArr[trailingGroupArr.length - 1] ?? '';
|
|
253
|
+
if (trailingGroup.includes('.')) {
|
|
254
|
+
const v4 = parseIPv4Literal(trailingGroup);
|
|
255
|
+
if (v4 === null)
|
|
256
|
+
return null;
|
|
257
|
+
v4Suffix = [(v4 >>> 16) & 0xff_ff, v4 & 0xff_ff];
|
|
258
|
+
if (tail.length > 0)
|
|
259
|
+
tail = tail.slice(0, -1);
|
|
260
|
+
else
|
|
261
|
+
head = head.slice(0, -1);
|
|
262
|
+
}
|
|
263
|
+
const headGroups = head.map(parseHextet);
|
|
264
|
+
const tailGroups = tail.map(parseHextet);
|
|
265
|
+
if (headGroups.includes(null) || tailGroups.includes(null))
|
|
266
|
+
return null;
|
|
267
|
+
const v4Count = v4Suffix ? 2 : 0;
|
|
268
|
+
if (doubleColon >= 0) {
|
|
269
|
+
const fillCount = 8 - headGroups.length - tailGroups.length - v4Count;
|
|
270
|
+
if (fillCount < 0)
|
|
271
|
+
return null;
|
|
272
|
+
const zeros = new Array(fillCount).fill(0);
|
|
273
|
+
const result = [
|
|
274
|
+
...headGroups,
|
|
275
|
+
...zeros,
|
|
276
|
+
...tailGroups,
|
|
277
|
+
...(v4Suffix ?? []),
|
|
278
|
+
];
|
|
279
|
+
return result.length === 8 ? result : null;
|
|
280
|
+
}
|
|
281
|
+
// No compression — must add up to exactly 8 hextets including v4 suffix.
|
|
282
|
+
const result = [...headGroups, ...(v4Suffix ?? [])];
|
|
283
|
+
return result.length === 8 ? result : null;
|
|
284
|
+
}
|
|
285
|
+
function parseHextet(s) {
|
|
286
|
+
if (s.length === 0 || s.length > 4)
|
|
287
|
+
return null;
|
|
288
|
+
if (!/^[0-9a-fA-F]+$/.test(s))
|
|
289
|
+
return null;
|
|
290
|
+
return parseInt(s, 16);
|
|
291
|
+
}
|
|
292
|
+
function formatIPv6(groups) {
|
|
293
|
+
return groups.map((g) => g.toString(16)).join(':');
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* If `groups` represents an IPv4-mapped (`::ffff:a.b.c.d`) or non-trivial
|
|
297
|
+
* IPv4-compatible (`::a.b.c.d` with a.b.c.d != ::1, deprecated) IPv6 address,
|
|
298
|
+
* return the inner 32-bit IPv4 value.
|
|
299
|
+
*
|
|
300
|
+
* IMPORTANT: We deliberately do NOT extract from `::1` (loopback) or `::`
|
|
301
|
+
* (unspecified). Those are pure IPv6 addresses, not tunneled IPv4 — treating
|
|
302
|
+
* `::1` as `0.0.0.1` would silently allow loopback bypass via the IPv4
|
|
303
|
+
* range checks (which don't include `0.0.0.1`).
|
|
304
|
+
*/
|
|
305
|
+
function extractIPv4FromIPv6(groups) {
|
|
306
|
+
// First 5 groups must be all zero (the high-order 80 bits).
|
|
307
|
+
if (groups.slice(0, 5).some((g) => g !== 0))
|
|
308
|
+
return null;
|
|
309
|
+
const isMapped = groups[5] === 0xff_ff;
|
|
310
|
+
if (isMapped) {
|
|
311
|
+
return (((groups[6] ?? 0) << 16) | (groups[7] ?? 0)) >>> 0;
|
|
312
|
+
}
|
|
313
|
+
// IPv4-compatible (deprecated by RFC4291 §2.5.5.1): only when groups[5]===0
|
|
314
|
+
// AND the embedded "IPv4" is itself a real public-looking address (i.e.
|
|
315
|
+
// the whole thing isn't just `::N` for small N). We require the high
|
|
316
|
+
// hextet of the IPv4 portion (groups[6]) to be non-zero — that excludes
|
|
317
|
+
// `::1`, `::`, `::N` for N < 65536, all of which should be treated as
|
|
318
|
+
// pure IPv6 by their own range checks (loopback, unspecified, etc).
|
|
319
|
+
const isCompat = groups[5] === 0 && (groups[6] ?? 0) !== 0;
|
|
320
|
+
if (isCompat) {
|
|
321
|
+
return (((groups[6] ?? 0) << 16) | (groups[7] ?? 0)) >>> 0;
|
|
322
|
+
}
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
function checkPrivateIPv6(groups) {
|
|
326
|
+
// ::1 loopback
|
|
327
|
+
if (groups.every((g, i) => (i === 7 ? g === 1 : g === 0))) {
|
|
328
|
+
return { reason: '::1 (IPv6 loopback)' };
|
|
329
|
+
}
|
|
330
|
+
// :: unspecified
|
|
331
|
+
if (groups.every((g) => g === 0)) {
|
|
332
|
+
return { reason: ':: (unspecified)' };
|
|
333
|
+
}
|
|
334
|
+
// fc00::/7 unique local
|
|
335
|
+
if ((groups[0] & 0xfe_00) === 0xfc_00) {
|
|
336
|
+
return { reason: 'fc00::/7 (IPv6 unique local)' };
|
|
337
|
+
}
|
|
338
|
+
// fe80::/10 link-local
|
|
339
|
+
if ((groups[0] & 0xff_c0) === 0xfe_80) {
|
|
340
|
+
return { reason: 'fe80::/10 (IPv6 link-local)' };
|
|
341
|
+
}
|
|
342
|
+
// ff00::/8 multicast
|
|
343
|
+
if ((groups[0] & 0xff_00) === 0xff_00) {
|
|
344
|
+
return { reason: 'ff00::/8 (IPv6 multicast)' };
|
|
345
|
+
}
|
|
346
|
+
// 64:ff9b::/96 well-known IPv4/IPv6 translation
|
|
347
|
+
if (groups[0] === 0x00_64 && groups[1] === 0xff_9b && groups.slice(2, 6).every((g) => g === 0)) {
|
|
348
|
+
return { reason: '64:ff9b::/96 (IPv4-IPv6 translation)' };
|
|
349
|
+
}
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
//# sourceMappingURL=ip-canon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ip-canon.js","sourceRoot":"","sources":["../../src/security/ip-canon.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,9 +16,5 @@ export interface RateLimitConfig {
|
|
|
16
16
|
export declare function createInMemoryRateLimiter(config: RateLimitConfig): RateLimiter;
|
|
17
17
|
/** Create a rate limiter backed by Upstash Redis (for serverless/production). */
|
|
18
18
|
export declare function createUpstashRateLimiter(config: RateLimitConfig): RateLimiter;
|
|
19
|
-
/**
|
|
20
|
-
* Create a rate limiter with automatic backend detection.
|
|
21
|
-
* Uses Upstash Redis if UPSTASH_REDIS_REST_URL is set, otherwise falls back to in-memory.
|
|
22
|
-
*/
|
|
23
19
|
export declare function createRateLimiter(config: RateLimitConfig): RateLimiter;
|
|
24
20
|
//# sourceMappingURL=rate-limit.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/security/rate-limit.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,IAAI,CAAA;IACb,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;IAC5C,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,4FAA4F;AAC5F,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,eAAe,GAAG,WAAW,CA4B9E;AAED,iFAAiF;AACjF,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,eAAe,GAAG,WAAW,CAqF7E;
|
|
1
|
+
{"version":3,"file":"rate-limit.d.ts","sourceRoot":"","sources":["../../src/security/rate-limit.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,IAAI,CAAA;IACb,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAAA;IAC5C,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAClC;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;CACpB;AAED,4FAA4F;AAC5F,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,eAAe,GAAG,WAAW,CA4B9E;AAED,iFAAiF;AACjF,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,eAAe,GAAG,WAAW,CAqF7E;AA6BD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,eAAe,GAAG,WAAW,CAkBtE"}
|
|
@@ -102,7 +102,37 @@ export function createUpstashRateLimiter(config) {
|
|
|
102
102
|
* Create a rate limiter with automatic backend detection.
|
|
103
103
|
* Uses Upstash Redis if UPSTASH_REDIS_REST_URL is set, otherwise falls back to in-memory.
|
|
104
104
|
*/
|
|
105
|
+
/**
|
|
106
|
+
* Returns a rate limiter that always permits requests.
|
|
107
|
+
*
|
|
108
|
+
* Useful for test harnesses (Playwright, Vitest, k6) where every test
|
|
109
|
+
* shares the same client-side IP — without a bypass, the first 5 login
|
|
110
|
+
* attempts exhaust the strict 5-per-15-minute bucket and every later
|
|
111
|
+
* test fails with HTTP 429. Production never sets the env var that
|
|
112
|
+
* activates this; the activation lives in `createRateLimiter` so every
|
|
113
|
+
* caller gets it without scattering env checks across the codebase.
|
|
114
|
+
*/
|
|
115
|
+
function createPermissiveRateLimiter(config) {
|
|
116
|
+
return {
|
|
117
|
+
async check() {
|
|
118
|
+
return {
|
|
119
|
+
allowed: true,
|
|
120
|
+
remaining: config.maxRequests,
|
|
121
|
+
resetAt: new Date(Date.now() + config.windowMs),
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
async reset() { },
|
|
125
|
+
};
|
|
126
|
+
}
|
|
105
127
|
export function createRateLimiter(config) {
|
|
128
|
+
// Explicit, environment-gated bypass for test harnesses. We deliberately
|
|
129
|
+
// do NOT key off `NODE_ENV === 'test'` because Next.js builds run with
|
|
130
|
+
// NODE_ENV=test in some CI configurations and we want unit tests that
|
|
131
|
+
// explicitly opt in to still exercise the real limiter logic. Vercel +
|
|
132
|
+
// the deploy guide both omit this var, so production never disables.
|
|
133
|
+
if (process.env.ACTUATE_DISABLE_RATE_LIMIT === '1') {
|
|
134
|
+
return createPermissiveRateLimiter(config);
|
|
135
|
+
}
|
|
106
136
|
if (process.env.UPSTASH_REDIS_REST_URL && process.env.UPSTASH_REDIS_REST_TOKEN) {
|
|
107
137
|
try {
|
|
108
138
|
return createUpstashRateLimiter(config);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/security/rate-limit.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../../src/security/rate-limit.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,21 +2,27 @@
|
|
|
2
2
|
* Perform a `fetch()` with SSRF defenses applied:
|
|
3
3
|
*
|
|
4
4
|
* - Reject non-http/https URLs (no `file:`, `gopher:`, `data:`).
|
|
5
|
-
* - Reject hostnames that resolve to private/loopback IP literals
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* - Reject hostnames that resolve to private/loopback IP literals via the
|
|
6
|
+
* canonicalizer in `ip-canon.ts` (decimal, octal, hex, IPv4-mapped IPv6,
|
|
7
|
+
* short-form, CGNAT, cloud metadata, etc.).
|
|
8
|
+
* - Reject `localhost`, `0.0.0.0`, and `*.internal` hostnames.
|
|
9
|
+
* - DNS-resolve the hostname before fetching and reject if any A/AAAA record
|
|
10
|
+
* lands in a private range. This defeats DNS rebinding — without it, an
|
|
11
|
+
* attacker controlling DNS for `evil.tld` returns `8.8.8.8` to our
|
|
12
|
+
* validator and `127.0.0.1` to the actual `fetch`. The pre-resolution
|
|
13
|
+
* forces the same lookup.
|
|
8
14
|
* - Disable HTTP redirects by default (`redirect: 'manual'`) — a 302 to an
|
|
9
15
|
* internal address would otherwise smuggle the request past the validator.
|
|
16
|
+
* When `followRedirects: true`, each hop is re-validated AND re-resolved.
|
|
10
17
|
* - Apply a hard request timeout via `AbortSignal.timeout`.
|
|
11
18
|
*
|
|
12
19
|
* Use this for any internal `fetch()` call that targets a URL derived from
|
|
13
20
|
* user/admin input (webhooks, link health, image URL fetches, etc).
|
|
14
21
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* protection for portability across runtimes (Edge, Bun, Workers).
|
|
22
|
+
* **Runtime caveat:** DNS resolution uses `node:dns/promises`. On Edge / Bun
|
|
23
|
+
* / Workers without that module, `resolveDns` is skipped and only the
|
|
24
|
+
* URL-string check applies — set `requireDnsCheck: true` to fail closed on
|
|
25
|
+
* runtimes where the resolver isn't available.
|
|
20
26
|
*/
|
|
21
27
|
export interface SafeFetchOptions extends RequestInit {
|
|
22
28
|
/** Hard timeout applied via AbortSignal. Defaults to 5000ms. */
|
|
@@ -25,6 +31,22 @@ export interface SafeFetchOptions extends RequestInit {
|
|
|
25
31
|
followRedirects?: boolean;
|
|
26
32
|
/** Maximum number of redirect hops when followRedirects is true. Default: 3. */
|
|
27
33
|
maxRedirects?: number;
|
|
34
|
+
/**
|
|
35
|
+
* When true, DNS resolution failures (including missing `node:dns/promises`)
|
|
36
|
+
* cause the request to fail closed with an `SsrfBlockedError` instead of
|
|
37
|
+
* proceeding with only the string-level check. Default: false (best effort
|
|
38
|
+
* — preserves portability across Edge/Workers runtimes).
|
|
39
|
+
*/
|
|
40
|
+
requireDnsCheck?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Test seam — allows tests to substitute a fake resolver instead of hitting
|
|
43
|
+
* real DNS. Production callers leave this undefined.
|
|
44
|
+
*/
|
|
45
|
+
_resolver?: (hostname: string) => Promise<{
|
|
46
|
+
safe: boolean;
|
|
47
|
+
resolvedIp?: string;
|
|
48
|
+
error?: string;
|
|
49
|
+
}>;
|
|
28
50
|
}
|
|
29
51
|
export declare class SsrfBlockedError extends Error {
|
|
30
52
|
readonly url: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-fetch.d.ts","sourceRoot":"","sources":["../../src/security/safe-fetch.ts"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"safe-fetch.d.ts","sourceRoot":"","sources":["../../src/security/safe-fetch.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,WAAW,gBAAiB,SAAQ,WAAW;IACnD,gEAAgE;IAChE,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sFAAsF;IACtF,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAClG;AAED,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;gBACX,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAMxC;AAuCD,wBAAsB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,QAAQ,CAAC,CA0C9F"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { validateWebhookUrl } from './webhook.js';
|
|
1
|
+
import { resolveAndCheck, validateWebhookUrl } from './webhook.js';
|
|
2
2
|
export class SsrfBlockedError extends Error {
|
|
3
3
|
url;
|
|
4
4
|
reason;
|
|
@@ -9,15 +9,41 @@ export class SsrfBlockedError extends Error {
|
|
|
9
9
|
this.reason = reason;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
+
async function preflightUrl(url, options) {
|
|
13
|
+
const check = validateWebhookUrl(url);
|
|
14
|
+
if (!check.valid) {
|
|
15
|
+
throw new SsrfBlockedError(url, check.error ?? 'URL rejected by SSRF policy');
|
|
16
|
+
}
|
|
17
|
+
let parsed;
|
|
18
|
+
try {
|
|
19
|
+
parsed = new URL(url);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
throw new SsrfBlockedError(url, 'invalid URL');
|
|
23
|
+
}
|
|
24
|
+
// resolveAndCheck short-circuits when the hostname is already an IP literal
|
|
25
|
+
// (which validateWebhookUrl just covered), so this only does real DNS work
|
|
26
|
+
// for actual hostnames.
|
|
27
|
+
let resolved;
|
|
28
|
+
try {
|
|
29
|
+
resolved = await options.resolver(parsed.hostname);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
if (options.requireDnsCheck) {
|
|
33
|
+
throw new SsrfBlockedError(url, `DNS resolution failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
34
|
+
}
|
|
35
|
+
return; // best-effort mode: proceed with string check only
|
|
36
|
+
}
|
|
37
|
+
if (!resolved.safe) {
|
|
38
|
+
throw new SsrfBlockedError(url, resolved.error ?? 'hostname resolves to a private IP');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
12
41
|
export async function safeFetch(url, options = {}) {
|
|
13
|
-
const { timeoutMs = 5000, followRedirects = false, maxRedirects = 3, ...init } = options;
|
|
42
|
+
const { timeoutMs = 5000, followRedirects = false, maxRedirects = 3, requireDnsCheck = false, _resolver = resolveAndCheck, ...init } = options;
|
|
14
43
|
let currentUrl = url;
|
|
15
44
|
let hops = 0;
|
|
16
45
|
while (true) {
|
|
17
|
-
|
|
18
|
-
if (!check.valid) {
|
|
19
|
-
throw new SsrfBlockedError(currentUrl, check.error ?? 'URL rejected by SSRF policy');
|
|
20
|
-
}
|
|
46
|
+
await preflightUrl(currentUrl, { requireDnsCheck, resolver: _resolver });
|
|
21
47
|
const response = await fetch(currentUrl, {
|
|
22
48
|
...init,
|
|
23
49
|
redirect: 'manual',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe-fetch.js","sourceRoot":"","sources":["../../src/security/safe-fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"safe-fetch.js","sourceRoot":"","sources":["../../src/security/safe-fetch.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,9 +1,27 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Validate that a webhook URL does not target private/internal networks
|
|
3
|
+
* (string-level SSRF prevention).
|
|
4
|
+
*
|
|
5
|
+
* Catches every IP-literal encoding `getaddrinfo` accepts: decimal
|
|
6
|
+
* (`http://2130706433`), octal (`http://0177.0.0.1`), hex
|
|
7
|
+
* (`http://0x7f.0.0.1`), short-form (`http://127.1`), IPv4-mapped IPv6
|
|
8
|
+
* (`http://[::ffff:127.0.0.1]`), bracketed IPv6, and the reserved
|
|
9
|
+
* `100.64.0.0/10` carrier-grade NAT range that previously slipped through.
|
|
10
|
+
*
|
|
11
|
+
* **Important caveat:** this only validates the URL **as written**. A hostname
|
|
12
|
+
* like `attacker-controlled.tld` that resolves to a public IP at validation
|
|
13
|
+
* time and to `127.0.0.1` at fetch time (DNS rebinding) still bypasses this
|
|
14
|
+
* check — `safeFetch` calls `resolveAndCheck()` on top to defend against that.
|
|
15
|
+
*/
|
|
2
16
|
export declare function validateWebhookUrl(url: string): {
|
|
3
17
|
valid: boolean;
|
|
4
18
|
error?: string;
|
|
5
19
|
};
|
|
6
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a hostname's A/AAAA records and verify none of them land in a
|
|
22
|
+
* private range. Pair with `validateWebhookUrl` to defend against DNS
|
|
23
|
+
* rebinding — see `safeFetch`.
|
|
24
|
+
*/
|
|
7
25
|
export declare function resolveAndCheck(hostname: string): Promise<{
|
|
8
26
|
safe: boolean;
|
|
9
27
|
resolvedIp?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/security/webhook.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../../src/security/webhook.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAmDlF;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAgEjE"}
|