@zudojs/security 1.0.0 → 1.1.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/README.md +56 -5
- package/dist/body/body.core.d.ts +6 -0
- package/dist/body/body.core.js +24 -4
- package/dist/body/body.guard.d.ts +19 -0
- package/dist/body/body.guard.js +26 -0
- package/dist/cookie/cookie.core.d.ts +10 -6
- package/dist/cookie/cookie.core.js +22 -20
- package/dist/cookie/cookie.sensitive.d.ts +27 -0
- package/dist/cookie/cookie.sensitive.js +61 -0
- package/dist/cookie/index.d.ts +1 -0
- package/dist/cookie/index.js +1 -0
- package/dist/cors/cors.core.js +2 -1
- package/dist/csrf/csrf.core.d.ts +3 -1
- package/dist/csrf/csrf.core.js +19 -5
- package/dist/headers/headers.core.js +2 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/input/input.core.js +10 -1
- package/dist/input/input.decode.d.ts +25 -0
- package/dist/input/input.decode.js +72 -0
- package/dist/rateLimit/index.d.ts +9 -2
- package/dist/rateLimit/index.js +6 -1
- package/dist/rateLimit/rateLimit.clientIp.d.ts +40 -0
- package/dist/rateLimit/rateLimit.clientIp.js +63 -0
- package/dist/rateLimit/rateLimit.clientKey.d.ts +51 -0
- package/dist/rateLimit/rateLimit.clientKey.js +104 -0
- package/dist/rateLimit/rateLimit.core.d.ts +5 -30
- package/dist/rateLimit/rateLimit.core.js +37 -84
- package/dist/rateLimit/rateLimit.namespace.d.ts +2 -1
- package/dist/rateLimit/rateLimit.namespace.js +2 -1
- package/dist/types/security.type.d.ts +1 -1
- package/dist/types/security.type.js +3 -0
- package/dist/url/index.d.ts +1 -0
- package/dist/url/index.js +1 -0
- package/dist/url/url.core.js +12 -27
- package/dist/url/url.ipv6.d.ts +36 -0
- package/dist/url/url.ipv6.js +88 -0
- package/package.json +7 -3
|
@@ -3,21 +3,30 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Implements sliding window rate limiting to prevent abuse.
|
|
5
5
|
*/
|
|
6
|
+
import { createIpKeyGenerator, ipRateLimitKey } from "./rateLimit.clientKey.js";
|
|
6
7
|
/** Default window: 1 minute. */
|
|
7
8
|
const DEFAULT_WINDOW_MS = 60_000;
|
|
8
9
|
/** Default rate limit message. */
|
|
9
10
|
const DEFAULT_MESSAGE = "Too many requests";
|
|
10
11
|
/** Default cap on tracked keys before least-recently-seen eviction. */
|
|
11
12
|
const DEFAULT_MAX_KEYS = 100_000;
|
|
13
|
+
/** Largest delay `setInterval` honours without overflowing to 1 ms. */
|
|
14
|
+
const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
12
15
|
/**
|
|
13
16
|
* Default key generator using IP address.
|
|
14
17
|
*
|
|
18
|
+
* The port is stripped, IPv4-mapped IPv6 is keyed as IPv4, and IPv6 is
|
|
19
|
+
* bucketed by /64 (see {@link createIpKeyGenerator} for another prefix).
|
|
20
|
+
*
|
|
15
21
|
* @param request - The rate limit request.
|
|
16
22
|
* @returns The rate limit key.
|
|
23
|
+
* @throws {ConfigurationError} when `request.ip` is missing or not an IP
|
|
24
|
+
* address; requests used to share a single `"unknown"` bucket.
|
|
17
25
|
*/
|
|
18
26
|
export function defaultKeyGenerator(request) {
|
|
19
|
-
return request
|
|
27
|
+
return ipKeyGenerator(request);
|
|
20
28
|
}
|
|
29
|
+
const ipKeyGenerator = createIpKeyGenerator();
|
|
21
30
|
/**
|
|
22
31
|
* Default handler when rate limit is exceeded.
|
|
23
32
|
*
|
|
@@ -79,6 +88,8 @@ export function createRateLimiter(config) {
|
|
|
79
88
|
}
|
|
80
89
|
const store = new Map();
|
|
81
90
|
const keyGenerator = config.keyGenerator ?? defaultKeyGenerator;
|
|
91
|
+
/** Lets `reset`/`getCount` take a raw address under the default keys. */
|
|
92
|
+
const storeKey = (key) => config.keyGenerator || store.has(key) ? key : (ipRateLimitKey(key) ?? key);
|
|
82
93
|
// `config.message` was declared and documented but never read: the default
|
|
83
94
|
// handler always emitted the built-in string.
|
|
84
95
|
const message = config.message ?? DEFAULT_MESSAGE;
|
|
@@ -88,8 +99,11 @@ export function createRateLimiter(config) {
|
|
|
88
99
|
});
|
|
89
100
|
const skip = config.skip;
|
|
90
101
|
const maxKeys = config.maxKeys ?? DEFAULT_MAX_KEYS;
|
|
91
|
-
// Cleanup old entries periodically. Bounded so a very short window
|
|
92
|
-
// schedule a near-continuous timer
|
|
102
|
+
// Cleanup old entries periodically. Bounded below so a very short window
|
|
103
|
+
// does not schedule a near-continuous timer, and above because Node stores
|
|
104
|
+
// timer delays as a signed 32-bit integer: a delay past 2^31 - 1 ms (about
|
|
105
|
+
// 24.8 days) is silently replaced with 1 ms, so a month-long window used to
|
|
106
|
+
// sweep the whole store a thousand times a second.
|
|
93
107
|
const cleanupInterval = setInterval(() => {
|
|
94
108
|
const cutoff = Date.now() - config.windowMs;
|
|
95
109
|
for (const [key, entry] of store) {
|
|
@@ -97,7 +111,7 @@ export function createRateLimiter(config) {
|
|
|
97
111
|
store.delete(key);
|
|
98
112
|
}
|
|
99
113
|
}
|
|
100
|
-
}, Math.max(config.windowMs, 1_000));
|
|
114
|
+
}, Math.min(Math.max(config.windowMs, 1_000), MAX_TIMER_DELAY_MS));
|
|
101
115
|
// Allow cleanup to not keep process alive
|
|
102
116
|
if (cleanupInterval.unref) {
|
|
103
117
|
cleanupInterval.unref();
|
|
@@ -107,16 +121,20 @@ export function createRateLimiter(config) {
|
|
|
107
121
|
*
|
|
108
122
|
* Without this, a caller rotating the key (a spoofed forwarding header, a
|
|
109
123
|
* per-request identifier) grows the map without limit between sweeps.
|
|
124
|
+
*
|
|
125
|
+
* The store is kept in recency order — `check` re-inserts an entry on every
|
|
126
|
+
* hit — so the oldest key is always the first one iterated and eviction is
|
|
127
|
+
* O(1). It used to copy and sort the whole map on every new key past the
|
|
128
|
+
* cap, which turned the defence against key rotation into an O(n log n)
|
|
129
|
+
* cost per rotated request: the attack it was meant to bound became the
|
|
130
|
+
* cheapest way to burn the CPU.
|
|
110
131
|
*/
|
|
111
132
|
function evictIfNeeded() {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const entry = entries[i];
|
|
118
|
-
if (entry)
|
|
119
|
-
store.delete(entry[0]);
|
|
133
|
+
while (store.size > maxKeys) {
|
|
134
|
+
const oldest = store.keys().next();
|
|
135
|
+
if (oldest.done)
|
|
136
|
+
break;
|
|
137
|
+
store.delete(oldest.value);
|
|
120
138
|
}
|
|
121
139
|
}
|
|
122
140
|
/**
|
|
@@ -141,6 +159,11 @@ export function createRateLimiter(config) {
|
|
|
141
159
|
store.set(key, entry);
|
|
142
160
|
evictIfNeeded();
|
|
143
161
|
}
|
|
162
|
+
else {
|
|
163
|
+
// Move to the most-recent end so eviction order stays least-recent-first.
|
|
164
|
+
store.delete(key);
|
|
165
|
+
store.set(key, entry);
|
|
166
|
+
}
|
|
144
167
|
// Prune everything that has slid out of the window.
|
|
145
168
|
const timestamps = entry.timestamps.filter((t) => t > windowStart);
|
|
146
169
|
entry.lastSeen = now;
|
|
@@ -177,7 +200,7 @@ export function createRateLimiter(config) {
|
|
|
177
200
|
* Resets the rate limit for a specific key.
|
|
178
201
|
*/
|
|
179
202
|
function reset(key) {
|
|
180
|
-
store.delete(key);
|
|
203
|
+
store.delete(storeKey(key));
|
|
181
204
|
}
|
|
182
205
|
/**
|
|
183
206
|
* Clears all rate limit data.
|
|
@@ -189,7 +212,7 @@ export function createRateLimiter(config) {
|
|
|
189
212
|
* Gets the current count for a key.
|
|
190
213
|
*/
|
|
191
214
|
function getCount(key) {
|
|
192
|
-
const entry = store.get(key);
|
|
215
|
+
const entry = store.get(storeKey(key));
|
|
193
216
|
if (!entry)
|
|
194
217
|
return 0;
|
|
195
218
|
const windowStart = Date.now() - config.windowMs;
|
|
@@ -215,74 +238,4 @@ export function createRateLimiter(config) {
|
|
|
215
238
|
},
|
|
216
239
|
};
|
|
217
240
|
}
|
|
218
|
-
/**
|
|
219
|
-
* Extracts the client IP from request headers.
|
|
220
|
-
*
|
|
221
|
-
* **Forwarding headers are not trusted by default.** Any client can send
|
|
222
|
-
* `X-Forwarded-For`, so taking its leftmost entry — the historical behaviour —
|
|
223
|
-
* hands the caller control of their own rate-limit bucket, and rotating it
|
|
224
|
-
* defeats the limiter entirely. Pass `trustProxy` set to the number of proxies
|
|
225
|
-
* you actually run, together with the socket's `remoteAddress`.
|
|
226
|
-
*
|
|
227
|
-
* @param headers - Request headers.
|
|
228
|
-
* @param options - Proxy trust configuration.
|
|
229
|
-
* @returns The client IP address, or "unknown".
|
|
230
|
-
*/
|
|
231
|
-
export function extractClientIp(headers, options) {
|
|
232
|
-
const trustProxy = options?.trustProxy ?? 0;
|
|
233
|
-
const fallback = options?.remoteAddress ?? "unknown";
|
|
234
|
-
if (trustProxy <= 0) {
|
|
235
|
-
return fallback;
|
|
236
|
-
}
|
|
237
|
-
const raw = lookupHeader(headers, "x-forwarded-for");
|
|
238
|
-
if (raw !== undefined) {
|
|
239
|
-
const chain = raw
|
|
240
|
-
.split(",")
|
|
241
|
-
.map((entry) => entry.trim())
|
|
242
|
-
.filter((entry) => entry.length > 0);
|
|
243
|
-
// Walk in from the right: index 0 from the end is the address our own
|
|
244
|
-
// outermost proxy observed, and each additional trusted hop steps left.
|
|
245
|
-
const index = chain.length - trustProxy;
|
|
246
|
-
const candidate = chain[Math.max(0, index)];
|
|
247
|
-
if (candidate && isPlausibleIp(candidate)) {
|
|
248
|
-
return candidate;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
const realIp = lookupHeader(headers, "x-real-ip");
|
|
252
|
-
if (realIp !== undefined && isPlausibleIp(realIp.trim())) {
|
|
253
|
-
return realIp.trim();
|
|
254
|
-
}
|
|
255
|
-
return fallback;
|
|
256
|
-
}
|
|
257
|
-
/** Case-insensitive header lookup that flattens repeated fields. */
|
|
258
|
-
function lookupHeader(headers, name) {
|
|
259
|
-
for (const key of Object.keys(headers)) {
|
|
260
|
-
if (key.toLowerCase() !== name)
|
|
261
|
-
continue;
|
|
262
|
-
const value = headers[key];
|
|
263
|
-
if (typeof value === "string")
|
|
264
|
-
return value;
|
|
265
|
-
if (Array.isArray(value) && value.length > 0)
|
|
266
|
-
return value.join(",");
|
|
267
|
-
}
|
|
268
|
-
return undefined;
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* Rejects values that are not addresses at all.
|
|
272
|
-
*
|
|
273
|
-
* A forwarding header is text, and a hostname or arbitrary string in it would
|
|
274
|
-
* otherwise become a rate-limit key of the attacker's choosing.
|
|
275
|
-
*/
|
|
276
|
-
function isPlausibleIp(value) {
|
|
277
|
-
const host = value.startsWith("[")
|
|
278
|
-
? value.slice(1, value.indexOf("]") === -1 ? undefined : value.indexOf("]"))
|
|
279
|
-
: value.split(":").length > 2
|
|
280
|
-
? value
|
|
281
|
-
: (value.split(":")[0] ?? value);
|
|
282
|
-
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) {
|
|
283
|
-
return host.split(".").every((octet) => Number(octet) <= 255);
|
|
284
|
-
}
|
|
285
|
-
// Any hex-and-colon string is accepted as an IPv6 candidate.
|
|
286
|
-
return /^[0-9a-fA-F:]+$/.test(host) && host.includes(":");
|
|
287
|
-
}
|
|
288
241
|
//# sourceMappingURL=rateLimit.core.js.map
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Convenience namespace for rate limiting utilities.
|
|
5
5
|
*/
|
|
6
|
-
import { defaultKeyGenerator, defaultHandler, retryAfterSeconds, createRateLimiter
|
|
6
|
+
import { defaultKeyGenerator, defaultHandler, retryAfterSeconds, createRateLimiter } from "./rateLimit.core.js";
|
|
7
|
+
import { extractClientIp } from "./rateLimit.clientIp.js";
|
|
7
8
|
export declare const rateLimit: {
|
|
8
9
|
defaultKeyGenerator: typeof defaultKeyGenerator;
|
|
9
10
|
defaultHandler: typeof defaultHandler;
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Convenience namespace for rate limiting utilities.
|
|
5
5
|
*/
|
|
6
|
-
import { defaultKeyGenerator, defaultHandler, retryAfterSeconds, createRateLimiter,
|
|
6
|
+
import { defaultKeyGenerator, defaultHandler, retryAfterSeconds, createRateLimiter, } from "./rateLimit.core.js";
|
|
7
|
+
import { extractClientIp } from "./rateLimit.clientIp.js";
|
|
7
8
|
export const rateLimit = {
|
|
8
9
|
defaultKeyGenerator,
|
|
9
10
|
defaultHandler,
|
|
@@ -207,7 +207,7 @@ export declare const PROTOTYPE_POLLUTION_KEYS: readonly ["__proto__", "construct
|
|
|
207
207
|
*
|
|
208
208
|
* Like {@link XSS_PATTERNS}, none carries the `g` flag — see the note there.
|
|
209
209
|
*/
|
|
210
|
-
export declare const SQL_INJECTION_PATTERNS: readonly [RegExp, RegExp, RegExp, RegExp];
|
|
210
|
+
export declare const SQL_INJECTION_PATTERNS: readonly [RegExp, RegExp, RegExp, RegExp, RegExp, RegExp, RegExp];
|
|
211
211
|
/**
|
|
212
212
|
* Common XSS patterns.
|
|
213
213
|
*
|
|
@@ -22,6 +22,9 @@ export const SQL_INJECTION_PATTERNS = [
|
|
|
22
22
|
/(\b(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|EXEC|EXECUTE|UNION|FETCH|DECLARE|TRUNCATE|COMMENT|ALTER)\b)/i,
|
|
23
23
|
/(--|#|\/\*|\*\/)/,
|
|
24
24
|
/('\s*(OR|AND)\s*')/i,
|
|
25
|
+
/'\s*(OR|AND)\b/i,
|
|
26
|
+
/'\s*\|\|/,
|
|
27
|
+
/\b(PG_SLEEP|SLEEP|BENCHMARK)\s*\(|\bWAITFOR\s+DELAY\b/i,
|
|
25
28
|
/(;\s*(DROP|DELETE|INSERT|UPDATE))/i,
|
|
26
29
|
];
|
|
27
30
|
/**
|
package/dist/url/index.d.ts
CHANGED
|
@@ -3,4 +3,5 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export { validateUrl, normalizePath, validateRequestTarget, isSafeUrl, isPrivateHostname, containsTraversal, fullyDecodeUri, } from "./url.core.js";
|
|
5
5
|
export type { RequestTargetConfig } from "./url.core.js";
|
|
6
|
+
export { expandIpv6, embeddedIpv4, isNonPublicIpv6Range, } from "./url.ipv6.js";
|
|
6
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/url/index.js
CHANGED
|
@@ -2,4 +2,5 @@
|
|
|
2
2
|
* @zudojs/security — URL Validation Barrel
|
|
3
3
|
*/
|
|
4
4
|
export { validateUrl, normalizePath, validateRequestTarget, isSafeUrl, isPrivateHostname, containsTraversal, fullyDecodeUri, } from "./url.core.js";
|
|
5
|
+
export { expandIpv6, embeddedIpv4, isNonPublicIpv6Range, } from "./url.ipv6.js";
|
|
5
6
|
//# sourceMappingURL=index.js.map
|
package/dist/url/url.core.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Validates and normalizes URLs, prevents path traversal attacks,
|
|
5
5
|
* and ensures request targets are safe.
|
|
6
6
|
*/
|
|
7
|
+
import { embeddedIpv4, expandIpv6, isNonPublicIpv6Range, } from "./url.ipv6.js";
|
|
7
8
|
/** Default maximum URL length. */
|
|
8
9
|
const DEFAULT_MAX_URL_LENGTH = 2048;
|
|
9
10
|
/** Default allowed protocols. */
|
|
@@ -301,36 +302,20 @@ function isPrivateIpv4(octets) {
|
|
|
301
302
|
/**
|
|
302
303
|
* True when an IPv6 hostname (already stripped of brackets) is private.
|
|
303
304
|
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
305
|
+
* Every form that embeds an IPv4 address — IPv4-compatible `::a.b.c.d`,
|
|
306
|
+
* mapped `::ffff:a.b.c.d`, translated `::ffff:0:a.b.c.d`, NAT64
|
|
307
|
+
* `64:ff9b::a.b.c.d` and 6to4 `2002::/16` — is judged as that IPv4
|
|
308
|
+
* address, whichever spelling it arrives in (WHATWG serialises
|
|
309
|
+
* `[::127.0.0.1]` as `[::7f00:1]`). An unparseable literal fails closed.
|
|
306
310
|
*/
|
|
307
311
|
function isPrivateIpv6(hostname) {
|
|
308
|
-
const
|
|
309
|
-
if (
|
|
312
|
+
const groups = expandIpv6(hostname);
|
|
313
|
+
if (!groups)
|
|
310
314
|
return true;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
return octets ? isPrivateIpv4(octets) : true;
|
|
316
|
-
}
|
|
317
|
-
// Hex-form IPv4-mapped: ::ffff:7f00:1
|
|
318
|
-
const hexMapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(host);
|
|
319
|
-
if (hexMapped?.[1] && hexMapped[2]) {
|
|
320
|
-
const high = parseInt(hexMapped[1], 16);
|
|
321
|
-
const low = parseInt(hexMapped[2], 16);
|
|
322
|
-
const octets = [high >> 8, high & 0xff, low >> 8, low & 0xff];
|
|
323
|
-
return isPrivateIpv4(octets);
|
|
324
|
-
}
|
|
325
|
-
const firstGroup = host.split(":")[0] ?? "";
|
|
326
|
-
const leading = parseInt(firstGroup.padEnd(4, "0"), 16);
|
|
327
|
-
// fc00::/7 unique local
|
|
328
|
-
if ((leading & 0xfe00) === 0xfc00)
|
|
329
|
-
return true;
|
|
330
|
-
// fe80::/10 link-local
|
|
331
|
-
if ((leading & 0xffc0) === 0xfe80)
|
|
332
|
-
return true;
|
|
333
|
-
return false;
|
|
315
|
+
const embedded = embeddedIpv4(groups);
|
|
316
|
+
if (embedded)
|
|
317
|
+
return isPrivateIpv4(embedded);
|
|
318
|
+
return isNonPublicIpv6Range(groups);
|
|
334
319
|
}
|
|
335
320
|
/**
|
|
336
321
|
* Hostnames that resolve inside the local network or to a metadata service.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/security — IPv6 literal parsing and embedded-IPv4 extraction.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Expands an IPv6 address into its eight 16-bit groups.
|
|
6
|
+
*
|
|
7
|
+
* Accepts the compressed (`::`), dotted-tail (`::ffff:1.2.3.4`) and zoned
|
|
8
|
+
* (`fe80::1%eth0`) forms.
|
|
9
|
+
*
|
|
10
|
+
* @returns The groups, or `undefined` for a non-IPv6 input.
|
|
11
|
+
*/
|
|
12
|
+
export declare function expandIpv6(address: string): number[] | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* The IPv4 address an IPv6 address stands for, when it is one of the
|
|
15
|
+
* embedding forms, so that it can be judged as that IPv4 address.
|
|
16
|
+
*
|
|
17
|
+
* - `::a.b.c.d` (IPv4-compatible, `::/96`, which also covers `::` and `::1`)
|
|
18
|
+
* - `::ffff:a.b.c.d` (IPv4-mapped, `::ffff:0:0/96`)
|
|
19
|
+
* - `::ffff:0:a.b.c.d` (IPv4-translated, `::ffff:0:0:0/96`)
|
|
20
|
+
* - `64:ff9b::a.b.c.d` (NAT64 well-known prefix, `64:ff9b::/96`)
|
|
21
|
+
* - `2002:aabb:ccdd::` (6to4, `2002::/16`)
|
|
22
|
+
*
|
|
23
|
+
* WHATWG URL parsing rewrites `[::127.0.0.1]` to `[::7f00:1]`, so matching
|
|
24
|
+
* on the dotted spelling (as the old regexes did) missed every one of these.
|
|
25
|
+
*
|
|
26
|
+
* @returns The four octets, or `undefined` when nothing is embedded.
|
|
27
|
+
*/
|
|
28
|
+
export declare function embeddedIpv4(groups: readonly number[]): number[] | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* True for IPv6 ranges that are never a public unicast destination:
|
|
31
|
+
* unique-local `fc00::/7`, link-local `fe80::/10`, deprecated site-local
|
|
32
|
+
* `fec0::/10`, multicast `ff00::/8`, and the local-use NAT64 prefix
|
|
33
|
+
* `64:ff9b:1::/48`, whose embedded address depends on local configuration.
|
|
34
|
+
*/
|
|
35
|
+
export declare function isNonPublicIpv6Range(groups: readonly number[]): boolean;
|
|
36
|
+
//# sourceMappingURL=url.ipv6.d.ts.map
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zudojs/security — IPv6 literal parsing and embedded-IPv4 extraction.
|
|
3
|
+
*/
|
|
4
|
+
import { isIPv6 } from "node:net";
|
|
5
|
+
/**
|
|
6
|
+
* Expands an IPv6 address into its eight 16-bit groups.
|
|
7
|
+
*
|
|
8
|
+
* Accepts the compressed (`::`), dotted-tail (`::ffff:1.2.3.4`) and zoned
|
|
9
|
+
* (`fe80::1%eth0`) forms.
|
|
10
|
+
*
|
|
11
|
+
* @returns The groups, or `undefined` for a non-IPv6 input.
|
|
12
|
+
*/
|
|
13
|
+
export function expandIpv6(address) {
|
|
14
|
+
let text = address.toLowerCase();
|
|
15
|
+
const zone = text.indexOf("%");
|
|
16
|
+
if (zone !== -1)
|
|
17
|
+
text = text.slice(0, zone);
|
|
18
|
+
if (!isIPv6(text))
|
|
19
|
+
return undefined;
|
|
20
|
+
const lastColon = text.lastIndexOf(":");
|
|
21
|
+
const tail = text.slice(lastColon + 1);
|
|
22
|
+
if (tail.includes(".")) {
|
|
23
|
+
const o = tail.split(".").map(Number);
|
|
24
|
+
const hi = ((o[0] << 8) | o[1]).toString(16);
|
|
25
|
+
const lo = ((o[2] << 8) | o[3]).toString(16);
|
|
26
|
+
text = `${text.slice(0, lastColon + 1)}${hi}:${lo}`;
|
|
27
|
+
}
|
|
28
|
+
const [head = "", rest] = text.split("::");
|
|
29
|
+
const left = head === "" ? [] : head.split(":");
|
|
30
|
+
const right = rest === undefined || rest === "" ? [] : rest.split(":");
|
|
31
|
+
const fill = rest === undefined
|
|
32
|
+
? []
|
|
33
|
+
: new Array(8 - left.length - right.length).fill("0");
|
|
34
|
+
return [...left, ...fill, ...right].map((group) => parseInt(group, 16));
|
|
35
|
+
}
|
|
36
|
+
function octetsOf(high, low) {
|
|
37
|
+
return [high >> 8, high & 0xff, low >> 8, low & 0xff];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The IPv4 address an IPv6 address stands for, when it is one of the
|
|
41
|
+
* embedding forms, so that it can be judged as that IPv4 address.
|
|
42
|
+
*
|
|
43
|
+
* - `::a.b.c.d` (IPv4-compatible, `::/96`, which also covers `::` and `::1`)
|
|
44
|
+
* - `::ffff:a.b.c.d` (IPv4-mapped, `::ffff:0:0/96`)
|
|
45
|
+
* - `::ffff:0:a.b.c.d` (IPv4-translated, `::ffff:0:0:0/96`)
|
|
46
|
+
* - `64:ff9b::a.b.c.d` (NAT64 well-known prefix, `64:ff9b::/96`)
|
|
47
|
+
* - `2002:aabb:ccdd::` (6to4, `2002::/16`)
|
|
48
|
+
*
|
|
49
|
+
* WHATWG URL parsing rewrites `[::127.0.0.1]` to `[::7f00:1]`, so matching
|
|
50
|
+
* on the dotted spelling (as the old regexes did) missed every one of these.
|
|
51
|
+
*
|
|
52
|
+
* @returns The four octets, or `undefined` when nothing is embedded.
|
|
53
|
+
*/
|
|
54
|
+
export function embeddedIpv4(groups) {
|
|
55
|
+
const [g0, g1, g2, g3, g4, g5, g6 = 0, g7 = 0] = groups;
|
|
56
|
+
const zeroTo = (end) => groups.slice(0, end).every((group) => group === 0);
|
|
57
|
+
if (zeroTo(6))
|
|
58
|
+
return octetsOf(g6, g7);
|
|
59
|
+
if (zeroTo(5) && g5 === 0xffff)
|
|
60
|
+
return octetsOf(g6, g7);
|
|
61
|
+
if (zeroTo(4) && g4 === 0xffff && g5 === 0)
|
|
62
|
+
return octetsOf(g6, g7);
|
|
63
|
+
if (g0 === 0x64 && g1 === 0xff9b && g2 === 0 && g3 === 0 && g4 === 0 && g5 === 0) {
|
|
64
|
+
return octetsOf(g6, g7);
|
|
65
|
+
}
|
|
66
|
+
if (g0 === 0x2002)
|
|
67
|
+
return octetsOf(g1 ?? 0, g2 ?? 0);
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* True for IPv6 ranges that are never a public unicast destination:
|
|
72
|
+
* unique-local `fc00::/7`, link-local `fe80::/10`, deprecated site-local
|
|
73
|
+
* `fec0::/10`, multicast `ff00::/8`, and the local-use NAT64 prefix
|
|
74
|
+
* `64:ff9b:1::/48`, whose embedded address depends on local configuration.
|
|
75
|
+
*/
|
|
76
|
+
export function isNonPublicIpv6Range(groups) {
|
|
77
|
+
const g0 = groups[0] ?? 0;
|
|
78
|
+
if ((g0 & 0xfe00) === 0xfc00)
|
|
79
|
+
return true;
|
|
80
|
+
if ((g0 & 0xffc0) === 0xfe80)
|
|
81
|
+
return true;
|
|
82
|
+
if ((g0 & 0xffc0) === 0xfec0)
|
|
83
|
+
return true;
|
|
84
|
+
if ((g0 & 0xff00) === 0xff00)
|
|
85
|
+
return true;
|
|
86
|
+
return g0 === 0x64 && groups[1] === 0xff9b && groups[2] === 1;
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=url.ipv6.js.map
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/security",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Security primitives for input validation, header security, CORS, CSRF protection, rate limiting, and security headers.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Oluwayemi Oyinlola",
|
|
8
|
+
"url": "https://github.com/oyinlola-tech"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"main": "./dist/index.js",
|
|
8
12
|
"module": "./dist/index.js",
|
|
@@ -20,8 +24,8 @@
|
|
|
20
24
|
"!dist/.tsbuildinfo"
|
|
21
25
|
],
|
|
22
26
|
"dependencies": {
|
|
23
|
-
"@zudojs/errors": "1.
|
|
24
|
-
"@zudojs/constants": "1.
|
|
27
|
+
"@zudojs/errors": "1.1.0",
|
|
28
|
+
"@zudojs/constants": "1.1.0"
|
|
25
29
|
},
|
|
26
30
|
"devDependencies": {
|
|
27
31
|
"typescript": "7.0.2",
|