@zudojs/security 1.0.1 → 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.
Files changed (38) 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 +3 -1
  14. package/dist/csrf/csrf.core.js +19 -5
  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.js +10 -1
  19. package/dist/input/input.decode.d.ts +25 -0
  20. package/dist/input/input.decode.js +72 -0
  21. package/dist/rateLimit/index.d.ts +9 -2
  22. package/dist/rateLimit/index.js +6 -1
  23. package/dist/rateLimit/rateLimit.clientIp.d.ts +40 -0
  24. package/dist/rateLimit/rateLimit.clientIp.js +63 -0
  25. package/dist/rateLimit/rateLimit.clientKey.d.ts +51 -0
  26. package/dist/rateLimit/rateLimit.clientKey.js +104 -0
  27. package/dist/rateLimit/rateLimit.core.d.ts +5 -30
  28. package/dist/rateLimit/rateLimit.core.js +12 -73
  29. package/dist/rateLimit/rateLimit.namespace.d.ts +2 -1
  30. package/dist/rateLimit/rateLimit.namespace.js +2 -1
  31. package/dist/types/security.type.d.ts +1 -1
  32. package/dist/types/security.type.js +3 -0
  33. package/dist/url/index.d.ts +1 -0
  34. package/dist/url/index.js +1 -0
  35. package/dist/url/url.core.js +12 -27
  36. package/dist/url/url.ipv6.d.ts +36 -0
  37. package/dist/url/url.ipv6.js +88 -0
  38. package/package.json +3 -3
@@ -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
@@ -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
- * 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.
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 host = hostname.toLowerCase();
309
- if (host === "::1" || host === "::" || host === "::0")
312
+ const groups = expandIpv6(hostname);
313
+ if (!groups)
310
314
  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;
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/security",
3
- "version": "1.0.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
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.1.0",
28
+ "@zudojs/constants": "1.1.0"
29
29
  },
30
30
  "devDependencies": {
31
31
  "typescript": "7.0.2",