@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.
Files changed (40) hide show
  1. package/README.md +56 -5
  2. package/dist/body/body.core.d.ts +6 -0
  3. package/dist/body/body.core.js +15 -1
  4. package/dist/body/body.guard.d.ts +19 -0
  5. package/dist/body/body.guard.js +26 -0
  6. package/dist/cookie/cookie.core.d.ts +10 -6
  7. package/dist/cookie/cookie.core.js +22 -20
  8. package/dist/cookie/cookie.sensitive.d.ts +27 -0
  9. package/dist/cookie/cookie.sensitive.js +61 -0
  10. package/dist/cookie/index.d.ts +1 -0
  11. package/dist/cookie/index.js +1 -0
  12. package/dist/cors/cors.core.js +2 -1
  13. package/dist/csrf/csrf.core.d.ts +5 -2
  14. package/dist/csrf/csrf.core.js +50 -6
  15. package/dist/headers/headers.core.js +2 -1
  16. package/dist/index.d.ts +4 -4
  17. package/dist/index.js +3 -3
  18. package/dist/input/input.core.d.ts +2 -0
  19. package/dist/input/input.core.js +34 -1
  20. package/dist/input/input.decode.d.ts +25 -0
  21. package/dist/input/input.decode.js +72 -0
  22. package/dist/rateLimit/index.d.ts +9 -2
  23. package/dist/rateLimit/index.js +6 -1
  24. package/dist/rateLimit/rateLimit.clientIp.d.ts +40 -0
  25. package/dist/rateLimit/rateLimit.clientIp.js +71 -0
  26. package/dist/rateLimit/rateLimit.clientKey.d.ts +51 -0
  27. package/dist/rateLimit/rateLimit.clientKey.js +104 -0
  28. package/dist/rateLimit/rateLimit.core.d.ts +5 -30
  29. package/dist/rateLimit/rateLimit.core.js +12 -73
  30. package/dist/rateLimit/rateLimit.namespace.d.ts +2 -1
  31. package/dist/rateLimit/rateLimit.namespace.js +2 -1
  32. package/dist/types/security.type.d.ts +1 -1
  33. package/dist/types/security.type.js +3 -0
  34. package/dist/url/index.d.ts +1 -0
  35. package/dist/url/index.js +1 -0
  36. package/dist/url/url.core.d.ts +6 -0
  37. package/dist/url/url.core.js +29 -30
  38. package/dist/url/url.ipv6.d.ts +36 -0
  39. package/dist/url/url.ipv6.js +88 -0
  40. 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.ip ?? "unknown";
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, extractClientIp } from "./rateLimit.core.js";
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, extractClientIp, } from "./rateLimit.core.js";
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
  /**
@@ -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
@@ -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
  */
@@ -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
- .split(/[/\\]/)
108
- .some((segment) => segment === ".." || segment === "...");
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
- * Also unwraps IPv4-mapped and IPv4-compatible forms, so `::ffff:127.0.0.1`
305
- * is recognised as loopback rather than treated as an opaque v6 address.
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 host = hostname.toLowerCase();
309
- if (host === "::1" || host === "::" || host === "::0")
326
+ const groups = expandIpv6(hostname);
327
+ if (!groups)
310
328
  return true;
311
- // IPv4-mapped (::ffff:127.0.0.1) and IPv4-compatible (::127.0.0.1)
312
- const mapped = /^::(?:ffff:)?(\d{1,3}(?:\.\d{1,3}){3})$/.exec(host);
313
- if (mapped?.[1]) {
314
- const octets = parseIpv4(mapped[1]);
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.1",
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.1",
28
- "@zudojs/constants": "1.0.1"
27
+ "@zudojs/errors": "1.2.0",
28
+ "@zudojs/constants": "1.1.1"
29
29
  },
30
30
  "devDependencies": {
31
31
  "typescript": "7.0.2",