@zudojs/security 1.0.1 → 1.2.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 +15 -1
- 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 +5 -2
- package/dist/csrf/csrf.core.js +50 -6
- 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.d.ts +2 -0
- package/dist/input/input.core.js +34 -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 +71 -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 +12 -73
- 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.d.ts +6 -0
- package/dist/url/url.core.js +29 -30
- package/dist/url/url.ipv6.d.ts +36 -0
- package/dist/url/url.ipv6.js +88 -0
- package/package.json +3 -3
|
@@ -7,8 +7,13 @@ import type { RateLimitConfig, RateLimitRequest, RateLimitResponse, RateLimitRes
|
|
|
7
7
|
/**
|
|
8
8
|
* Default key generator using IP address.
|
|
9
9
|
*
|
|
10
|
+
* The port is stripped, IPv4-mapped IPv6 is keyed as IPv4, and IPv6 is
|
|
11
|
+
* bucketed by /64 (see {@link createIpKeyGenerator} for another prefix).
|
|
12
|
+
*
|
|
10
13
|
* @param request - The rate limit request.
|
|
11
14
|
* @returns The rate limit key.
|
|
15
|
+
* @throws {ConfigurationError} when `request.ip` is missing or not an IP
|
|
16
|
+
* address; requests used to share a single `"unknown"` bucket.
|
|
12
17
|
*/
|
|
13
18
|
export declare function defaultKeyGenerator(request: RateLimitRequest): string;
|
|
14
19
|
/**
|
|
@@ -63,34 +68,4 @@ export declare function createRateLimiter(config: RateLimiterOptions): {
|
|
|
63
68
|
/** Number of keys currently tracked. */
|
|
64
69
|
readonly size: number;
|
|
65
70
|
};
|
|
66
|
-
/** Options controlling how far forwarding headers are trusted. */
|
|
67
|
-
export interface ClientIpOptions {
|
|
68
|
-
/**
|
|
69
|
-
* Number of reverse proxies you operate in front of this service.
|
|
70
|
-
*
|
|
71
|
-
* `X-Forwarded-For` is appended to by every hop, so the entries closest to
|
|
72
|
-
* the right are the ones your own infrastructure added. With `trustProxy: 1`
|
|
73
|
-
* the last entry is used, with `2` the second-to-last, and so on. Entries to
|
|
74
|
-
* the left of your proxies were supplied by the client and are ignored.
|
|
75
|
-
*
|
|
76
|
-
* Defaults to `0`: no forwarding header is trusted at all.
|
|
77
|
-
*/
|
|
78
|
-
readonly trustProxy?: number;
|
|
79
|
-
/** The connection's remote address, used when no header is trusted. */
|
|
80
|
-
readonly remoteAddress?: string;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Extracts the client IP from request headers.
|
|
84
|
-
*
|
|
85
|
-
* **Forwarding headers are not trusted by default.** Any client can send
|
|
86
|
-
* `X-Forwarded-For`, so taking its leftmost entry — the historical behaviour —
|
|
87
|
-
* hands the caller control of their own rate-limit bucket, and rotating it
|
|
88
|
-
* defeats the limiter entirely. Pass `trustProxy` set to the number of proxies
|
|
89
|
-
* you actually run, together with the socket's `remoteAddress`.
|
|
90
|
-
*
|
|
91
|
-
* @param headers - Request headers.
|
|
92
|
-
* @param options - Proxy trust configuration.
|
|
93
|
-
* @returns The client IP address, or "unknown".
|
|
94
|
-
*/
|
|
95
|
-
export declare function extractClientIp(headers: Record<string, string | string[] | undefined>, options?: ClientIpOptions): string;
|
|
96
71
|
//# sourceMappingURL=rateLimit.core.d.ts.map
|
|
@@ -3,6 +3,7 @@
|
|
|
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. */
|
|
@@ -14,12 +15,18 @@ const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
|
14
15
|
/**
|
|
15
16
|
* Default key generator using IP address.
|
|
16
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
|
+
*
|
|
17
21
|
* @param request - The rate limit request.
|
|
18
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.
|
|
19
25
|
*/
|
|
20
26
|
export function defaultKeyGenerator(request) {
|
|
21
|
-
return request
|
|
27
|
+
return ipKeyGenerator(request);
|
|
22
28
|
}
|
|
29
|
+
const ipKeyGenerator = createIpKeyGenerator();
|
|
23
30
|
/**
|
|
24
31
|
* Default handler when rate limit is exceeded.
|
|
25
32
|
*
|
|
@@ -81,6 +88,8 @@ export function createRateLimiter(config) {
|
|
|
81
88
|
}
|
|
82
89
|
const store = new Map();
|
|
83
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);
|
|
84
93
|
// `config.message` was declared and documented but never read: the default
|
|
85
94
|
// handler always emitted the built-in string.
|
|
86
95
|
const message = config.message ?? DEFAULT_MESSAGE;
|
|
@@ -191,7 +200,7 @@ export function createRateLimiter(config) {
|
|
|
191
200
|
* Resets the rate limit for a specific key.
|
|
192
201
|
*/
|
|
193
202
|
function reset(key) {
|
|
194
|
-
store.delete(key);
|
|
203
|
+
store.delete(storeKey(key));
|
|
195
204
|
}
|
|
196
205
|
/**
|
|
197
206
|
* Clears all rate limit data.
|
|
@@ -203,7 +212,7 @@ export function createRateLimiter(config) {
|
|
|
203
212
|
* Gets the current count for a key.
|
|
204
213
|
*/
|
|
205
214
|
function getCount(key) {
|
|
206
|
-
const entry = store.get(key);
|
|
215
|
+
const entry = store.get(storeKey(key));
|
|
207
216
|
if (!entry)
|
|
208
217
|
return 0;
|
|
209
218
|
const windowStart = Date.now() - config.windowMs;
|
|
@@ -229,74 +238,4 @@ export function createRateLimiter(config) {
|
|
|
229
238
|
},
|
|
230
239
|
};
|
|
231
240
|
}
|
|
232
|
-
/**
|
|
233
|
-
* Extracts the client IP from request headers.
|
|
234
|
-
*
|
|
235
|
-
* **Forwarding headers are not trusted by default.** Any client can send
|
|
236
|
-
* `X-Forwarded-For`, so taking its leftmost entry — the historical behaviour —
|
|
237
|
-
* hands the caller control of their own rate-limit bucket, and rotating it
|
|
238
|
-
* defeats the limiter entirely. Pass `trustProxy` set to the number of proxies
|
|
239
|
-
* you actually run, together with the socket's `remoteAddress`.
|
|
240
|
-
*
|
|
241
|
-
* @param headers - Request headers.
|
|
242
|
-
* @param options - Proxy trust configuration.
|
|
243
|
-
* @returns The client IP address, or "unknown".
|
|
244
|
-
*/
|
|
245
|
-
export function extractClientIp(headers, options) {
|
|
246
|
-
const trustProxy = options?.trustProxy ?? 0;
|
|
247
|
-
const fallback = options?.remoteAddress ?? "unknown";
|
|
248
|
-
if (trustProxy <= 0) {
|
|
249
|
-
return fallback;
|
|
250
|
-
}
|
|
251
|
-
const raw = lookupHeader(headers, "x-forwarded-for");
|
|
252
|
-
if (raw !== undefined) {
|
|
253
|
-
const chain = raw
|
|
254
|
-
.split(",")
|
|
255
|
-
.map((entry) => entry.trim())
|
|
256
|
-
.filter((entry) => entry.length > 0);
|
|
257
|
-
// Walk in from the right: index 0 from the end is the address our own
|
|
258
|
-
// outermost proxy observed, and each additional trusted hop steps left.
|
|
259
|
-
const index = chain.length - trustProxy;
|
|
260
|
-
const candidate = chain[Math.max(0, index)];
|
|
261
|
-
if (candidate && isPlausibleIp(candidate)) {
|
|
262
|
-
return candidate;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
const realIp = lookupHeader(headers, "x-real-ip");
|
|
266
|
-
if (realIp !== undefined && isPlausibleIp(realIp.trim())) {
|
|
267
|
-
return realIp.trim();
|
|
268
|
-
}
|
|
269
|
-
return fallback;
|
|
270
|
-
}
|
|
271
|
-
/** Case-insensitive header lookup that flattens repeated fields. */
|
|
272
|
-
function lookupHeader(headers, name) {
|
|
273
|
-
for (const key of Object.keys(headers)) {
|
|
274
|
-
if (key.toLowerCase() !== name)
|
|
275
|
-
continue;
|
|
276
|
-
const value = headers[key];
|
|
277
|
-
if (typeof value === "string")
|
|
278
|
-
return value;
|
|
279
|
-
if (Array.isArray(value) && value.length > 0)
|
|
280
|
-
return value.join(",");
|
|
281
|
-
}
|
|
282
|
-
return undefined;
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Rejects values that are not addresses at all.
|
|
286
|
-
*
|
|
287
|
-
* A forwarding header is text, and a hostname or arbitrary string in it would
|
|
288
|
-
* otherwise become a rate-limit key of the attacker's choosing.
|
|
289
|
-
*/
|
|
290
|
-
function isPlausibleIp(value) {
|
|
291
|
-
const host = value.startsWith("[")
|
|
292
|
-
? value.slice(1, value.indexOf("]") === -1 ? undefined : value.indexOf("]"))
|
|
293
|
-
: value.split(":").length > 2
|
|
294
|
-
? value
|
|
295
|
-
: (value.split(":")[0] ?? value);
|
|
296
|
-
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) {
|
|
297
|
-
return host.split(".").every((octet) => Number(octet) <= 255);
|
|
298
|
-
}
|
|
299
|
-
// Any hex-and-colon string is accepted as an IPv6 candidate.
|
|
300
|
-
return /^[0-9a-fA-F:]+$/.test(host) && host.includes(":");
|
|
301
|
-
}
|
|
302
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.d.ts
CHANGED
|
@@ -27,6 +27,12 @@ export declare function fullyDecodeUri(value: string): {
|
|
|
27
27
|
* Operates on the fully decoded form, and treats a backslash as a separator
|
|
28
28
|
* because Windows and some proxies do.
|
|
29
29
|
*
|
|
30
|
+
* RFC 3986 lets a path segment carry parameters after a `;`, and Tomcat,
|
|
31
|
+
* Jetty and several reverse-proxy pairings strip them before resolving the
|
|
32
|
+
* path — so `/a/..;/b` names the same resource as `/a/../b`. Each segment is
|
|
33
|
+
* therefore also tested with its parameters removed. The raw segment is still
|
|
34
|
+
* tested first, so nothing that was a traversal stops being one.
|
|
35
|
+
*
|
|
30
36
|
* @param path - The path to inspect.
|
|
31
37
|
* @returns True when a `..` segment is present.
|
|
32
38
|
*/
|
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. */
|
|
@@ -96,6 +97,12 @@ function decodeOnce(value) {
|
|
|
96
97
|
* Operates on the fully decoded form, and treats a backslash as a separator
|
|
97
98
|
* because Windows and some proxies do.
|
|
98
99
|
*
|
|
100
|
+
* RFC 3986 lets a path segment carry parameters after a `;`, and Tomcat,
|
|
101
|
+
* Jetty and several reverse-proxy pairings strip them before resolving the
|
|
102
|
+
* path — so `/a/..;/b` names the same resource as `/a/../b`. Each segment is
|
|
103
|
+
* therefore also tested with its parameters removed. The raw segment is still
|
|
104
|
+
* tested first, so nothing that was a traversal stops being one.
|
|
105
|
+
*
|
|
99
106
|
* @param path - The path to inspect.
|
|
100
107
|
* @returns True when a `..` segment is present.
|
|
101
108
|
*/
|
|
@@ -103,9 +110,17 @@ export function containsTraversal(path) {
|
|
|
103
110
|
const { decoded, truncated } = fullyDecodeUri(path);
|
|
104
111
|
if (truncated)
|
|
105
112
|
return true;
|
|
106
|
-
return decoded
|
|
107
|
-
|
|
108
|
-
|
|
113
|
+
return decoded.split(/[/\\]/).some((segment) => {
|
|
114
|
+
if (isTraversalSegment(segment))
|
|
115
|
+
return true;
|
|
116
|
+
const parameterStart = segment.indexOf(";");
|
|
117
|
+
return (parameterStart !== -1 &&
|
|
118
|
+
isTraversalSegment(segment.slice(0, parameterStart)));
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
/** True for the segments a path resolver walks upward on. */
|
|
122
|
+
function isTraversalSegment(segment) {
|
|
123
|
+
return segment === ".." || segment === "...";
|
|
109
124
|
}
|
|
110
125
|
/**
|
|
111
126
|
* Validates a URL against security configuration.
|
|
@@ -301,36 +316,20 @@ function isPrivateIpv4(octets) {
|
|
|
301
316
|
/**
|
|
302
317
|
* True when an IPv6 hostname (already stripped of brackets) is private.
|
|
303
318
|
*
|
|
304
|
-
*
|
|
305
|
-
*
|
|
319
|
+
* Every form that embeds an IPv4 address — IPv4-compatible `::a.b.c.d`,
|
|
320
|
+
* mapped `::ffff:a.b.c.d`, translated `::ffff:0:a.b.c.d`, NAT64
|
|
321
|
+
* `64:ff9b::a.b.c.d` and 6to4 `2002::/16` — is judged as that IPv4
|
|
322
|
+
* address, whichever spelling it arrives in (WHATWG serialises
|
|
323
|
+
* `[::127.0.0.1]` as `[::7f00:1]`). An unparseable literal fails closed.
|
|
306
324
|
*/
|
|
307
325
|
function isPrivateIpv6(hostname) {
|
|
308
|
-
const
|
|
309
|
-
if (
|
|
326
|
+
const groups = expandIpv6(hostname);
|
|
327
|
+
if (!groups)
|
|
310
328
|
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;
|
|
329
|
+
const embedded = embeddedIpv4(groups);
|
|
330
|
+
if (embedded)
|
|
331
|
+
return isPrivateIpv4(embedded);
|
|
332
|
+
return isNonPublicIpv6Range(groups);
|
|
334
333
|
}
|
|
335
334
|
/**
|
|
336
335
|
* 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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/security",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Security primitives for input validation, header security, CORS, CSRF protection, rate limiting, and security headers.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"!dist/.tsbuildinfo"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@zudojs/errors": "1.0
|
|
28
|
-
"@zudojs/constants": "1.
|
|
27
|
+
"@zudojs/errors": "1.2.0",
|
|
28
|
+
"@zudojs/constants": "1.1.1"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"typescript": "7.0.2",
|