@zudojs/security 1.2.0 → 1.3.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 CHANGED
@@ -155,14 +155,23 @@ Two patterns, both on the same HMAC-SHA256 token. Bind the token to a session
155
155
  wherever you have one — an unbound token is valid for every user.
156
156
 
157
157
  The shortest correct version binds your configuration once. `secret` must be at
158
- least 32 characters — the signature is HMAC-SHA256, so a shorter one adds no
159
- strength:
158
+ least 32 characters (`MIN_CSRF_SECRET_LENGTH`) — the signature is HMAC-SHA256,
159
+ so a shorter one adds no strength. Read it from the environment and refuse to
160
+ start without it. Never write `process.env.CSRF_SECRET ?? "some-string"`: a
161
+ hard-coded fallback is a secret anyone can read in your source, and a short
162
+ one throws at the first call. Generate one with
163
+ `node -e 'console.log(require("node:crypto").randomBytes(32).toString("hex"))'`.
160
164
 
161
165
  ```typescript
162
- import { createCsrfProtection } from "@zudojs/security";
166
+ import { createCsrfProtection, MIN_CSRF_SECRET_LENGTH } from "@zudojs/security";
167
+
168
+ const secret = process.env.CSRF_SECRET;
169
+ if (!secret || secret.length < MIN_CSRF_SECRET_LENGTH) {
170
+ throw new Error(`CSRF_SECRET must be set to at least ${MIN_CSRF_SECRET_LENGTH} characters`);
171
+ }
163
172
 
164
173
  const csrf = createCsrfProtection({
165
- secret: process.env.CSRF_SECRET, // >= 32 chars
174
+ secret,
166
175
  cookieName: "app_csrf",
167
176
  headerName: "x-app-csrf",
168
177
  expiration: 3600,
@@ -210,10 +219,16 @@ if (requiresCsrfProtection(request.method)) {
210
219
  }
211
220
  ```
212
221
 
213
- `validateCsrfToken` and `verifyDoubleSubmit` hold the secret to the same
214
- 32-character minimum as `generateCsrfToken`, and throw `ConfigurationError`
215
- otherwise — a verifier given `process.env.CSRF_SECRET ?? ""` used to accept
216
- tokens signed with an empty key.
222
+ **What returns `false` and what throws.** `csrf.verify`, `verifyDoubleSubmit`
223
+ and `validateCsrfToken` return `false` for anything wrong with the request: a
224
+ missing, malformed, non-string, forged, expired or mismatched token, a token
225
+ bound to another session, or a request with no method (treated as needing
226
+ protection). They throw `ConfigurationError` only for a configuration mistake:
227
+ a secret shorter than 32 characters — the same minimum `generateCsrfToken`
228
+ enforces, so a verifier given `process.env.CSRF_SECRET ?? ""` cannot accept
229
+ tokens signed with an empty key — or an empty or invalid `methods` list. A
230
+ throw from a CSRF check therefore means "fix your configuration", never "this
231
+ request is bad".
217
232
 
218
233
  The cookie is `Secure` and `HttpOnly` by default, which suits the synchroniser
219
234
  token pattern where the server renders the token into the page. For the
@@ -269,6 +284,13 @@ could never apply to it.
269
284
  an unsafe name, attribute, `Max-Age` or `Expires` throws. `SameSite=None` and
270
285
  `Partitioned` require `Secure`.
271
286
 
287
+ `Domain` must be a hostname — labels of letters, digits and hyphens separated
288
+ by dots, with an optional leading dot (`example.com`, `.example.com`) — and
289
+ anything else (spaces, colons, backslashes, empty labels) throws
290
+ `ValidationError`. `Path` may not contain a control character, `;` or `,`.
291
+ Previously only a real CR, LF, NUL, `;` or `,` was refused, so the literal text
292
+ `a\r\nX-Evil: 1` was written into `Domain=` unchanged.
293
+
272
294
  `parseCookieHeader` validates on the way in too. A name that is not an RFC 6265
273
295
  token, or a value carrying a control character, is reported in `errors` and
274
296
  kept out of `cookies` — it used to check only length, so a malformed cookie
@@ -0,0 +1,27 @@
1
+ /**
2
+ * @zudojs/security — Cookie attribute validation
3
+ *
4
+ * `Domain` and `Path` end up verbatim inside a `Set-Cookie` header, so each is
5
+ * held to a grammar rather than to a deny-list of a few characters. A
6
+ * deny-list of real CR/LF let through the literal text `a\r\nX-Evil: 1`
7
+ * (backslashes, spaces and a colon), which reaches any code that later
8
+ * unescapes or re-emits the header.
9
+ */
10
+ /**
11
+ * Validates a cookie `Domain` attribute as a hostname: labels of
12
+ * `[A-Za-z0-9-]` separated by dots, with an optional leading dot.
13
+ *
14
+ * @param domain - The Domain value.
15
+ * @throws {ValidationError} for anything else — spaces, colons, backslashes,
16
+ * control characters, empty labels, or an over-long name.
17
+ */
18
+ export declare function assertCookieDomain(domain: string): void;
19
+ /**
20
+ * Validates a cookie `Path` attribute.
21
+ *
22
+ * @param path - The Path value.
23
+ * @throws {ValidationError} when it contains a control character, DEL, `;`
24
+ * or `,`.
25
+ */
26
+ export declare function assertCookiePath(path: string): void;
27
+ //# sourceMappingURL=cookie.attribute.d.ts.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @zudojs/security — Cookie attribute validation
3
+ *
4
+ * `Domain` and `Path` end up verbatim inside a `Set-Cookie` header, so each is
5
+ * held to a grammar rather than to a deny-list of a few characters. A
6
+ * deny-list of real CR/LF let through the literal text `a\r\nX-Evil: 1`
7
+ * (backslashes, spaces and a colon), which reaches any code that later
8
+ * unescapes or re-emits the header.
9
+ */
10
+ import { ValidationError } from "@zudojs/errors";
11
+ /** One hostname label: 1–63 letters, digits or hyphens. */
12
+ const LABEL = "[A-Za-z0-9-]{1,63}";
13
+ /** A hostname with an optional leading dot: `example.com`, `.example.com`. */
14
+ const DOMAIN_PATTERN = new RegExp(`^\\.?${LABEL}(?:\\.${LABEL})*$`);
15
+ /** Longest hostname DNS allows, excluding the optional leading dot. */
16
+ const MAX_DOMAIN_LENGTH = 253;
17
+ /**
18
+ * Characters never allowed in `Path`: controls (CR and LF included), DEL,
19
+ * `;` which would end the attribute, and `,` which some parsers split
20
+ * `Set-Cookie` on.
21
+ */
22
+ const PATH_UNSAFE = /[\x00-\x1F\x7F;,]/;
23
+ /**
24
+ * Validates a cookie `Domain` attribute as a hostname: labels of
25
+ * `[A-Za-z0-9-]` separated by dots, with an optional leading dot.
26
+ *
27
+ * @param domain - The Domain value.
28
+ * @throws {ValidationError} for anything else — spaces, colons, backslashes,
29
+ * control characters, empty labels, or an over-long name.
30
+ */
31
+ export function assertCookieDomain(domain) {
32
+ const bare = typeof domain === "string" && domain.startsWith(".")
33
+ ? domain.slice(1)
34
+ : domain;
35
+ if (typeof domain !== "string" ||
36
+ !DOMAIN_PATTERN.test(domain) ||
37
+ bare.length > MAX_DOMAIN_LENGTH) {
38
+ throw new ValidationError(`Cookie Domain contains invalid characters (injection risk): it must be ` +
39
+ `a hostname (letters, digits, hyphens and dots, optionally with a ` +
40
+ `leading dot), got ${JSON.stringify(domain)}`);
41
+ }
42
+ }
43
+ /**
44
+ * Validates a cookie `Path` attribute.
45
+ *
46
+ * @param path - The Path value.
47
+ * @throws {ValidationError} when it contains a control character, DEL, `;`
48
+ * or `,`.
49
+ */
50
+ export function assertCookiePath(path) {
51
+ if (typeof path !== "string" || PATH_UNSAFE.test(path)) {
52
+ throw new ValidationError(`Cookie Path contains invalid characters (injection risk): control ` +
53
+ `characters, ";" and "," are not allowed, got ${JSON.stringify(path)}`);
54
+ }
55
+ }
56
+ //# sourceMappingURL=cookie.attribute.js.map
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { ValidationError } from "@zudojs/errors";
7
7
  import { DEFAULT_SENSITIVE_COOKIE_NAMES, isSensitiveCookieName, } from "./cookie.sensitive.js";
8
+ import { assertCookieDomain, assertCookiePath } from "./cookie.attribute.js";
8
9
  /** Maximum cookie header size (4KB). */
9
10
  const MAX_COOKIE_HEADER_SIZE = 4096;
10
11
  /** Maximum number of cookies. */
@@ -24,8 +25,6 @@ const COOKIE_NAME_PATTERN = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
24
25
  * quote, comma, semicolon, and backslash.
25
26
  */
26
27
  const COOKIE_VALUE_PATTERN = /^[\x21\x23-\x2B\x2D-\x3A\x3C-\x5B\x5D-\x7E]*$/;
27
- /** Characters that must never reach an attribute value. */
28
- const ATTRIBUTE_UNSAFE = /[;,\r\n\x00]/;
29
28
  /**
30
29
  * Control characters that are never acceptable inside a parsed cookie value,
31
30
  * however lenient the rest of the read path is.
@@ -121,11 +120,11 @@ export function serializeCookie(cookie, config) {
121
120
  }
122
121
  const parts = [`${cookie.name}=${encodedValue}`];
123
122
  if (cookie.path) {
124
- assertSafeAttribute("Path", cookie.path);
123
+ assertCookiePath(cookie.path);
125
124
  parts.push(`Path=${cookie.path}`);
126
125
  }
127
126
  if (cookie.domain) {
128
- assertSafeAttribute("Domain", cookie.domain);
127
+ assertCookieDomain(cookie.domain);
129
128
  parts.push(`Domain=${cookie.domain}`);
130
129
  }
131
130
  if (cookie.maxAge !== undefined) {
@@ -165,12 +164,6 @@ export function serializeCookie(cookie, config) {
165
164
  }
166
165
  return parts.join("; ");
167
166
  }
168
- /** Throws when an attribute value could terminate the attribute or the header. */
169
- function assertSafeAttribute(attribute, value) {
170
- if (ATTRIBUTE_UNSAFE.test(value)) {
171
- throw new ValidationError(`Cookie ${attribute} contains invalid characters (injection risk): ${JSON.stringify(value)}`);
172
- }
173
- }
174
167
  /**
175
168
  * Creates a Set-Cookie header value with secure defaults.
176
169
  *
@@ -17,6 +17,22 @@
17
17
  *
18
18
  * Bind the token to a session wherever you have one: without `sessionId`, a
19
19
  * token minted for one user validates for every other user.
20
+ *
21
+ * Error contract: the verifiers (`validateCsrfToken`, `verifyDoubleSubmit`,
22
+ * `CsrfProtection.verify`) return `false` for anything wrong with the
23
+ * *request* — a missing, malformed, non-string, forged, expired or
24
+ * mismatched token, or a request with no method. They throw
25
+ * `ConfigurationError` only for a *configuration* mistake: a secret shorter
26
+ * than {@link MIN_CSRF_SECRET_LENGTH} (32) characters, or an unusable
27
+ * `methods` list. Load the secret from the environment and refuse to start
28
+ * without it; never fall back to a hard-coded string:
29
+ *
30
+ * ```typescript
31
+ * const secret = process.env.CSRF_SECRET;
32
+ * if (!secret || secret.length < MIN_CSRF_SECRET_LENGTH) {
33
+ * throw new Error("Set CSRF_SECRET to at least 32 random characters.");
34
+ * }
35
+ * ```
20
36
  */
21
37
  import type { CsrfConfig } from "../types/security.type.js";
22
38
  /**
@@ -73,7 +89,10 @@ export declare function validateCsrfToken(token: string, secret: string, options
73
89
  * @param requestToken - Token taken from the request header or form field.
74
90
  * @param secret - The secret key for verification.
75
91
  * @param options - Maximum lifetime and session binding.
76
- * @returns True when the request carries a matching, valid token.
92
+ * @returns True when the request carries a matching, valid token; `false`
93
+ * for a missing, non-string, mismatched, forged or expired one.
94
+ * @throws {ConfigurationError} only when the secret is empty or shorter than
95
+ * {@link MIN_CSRF_SECRET_LENGTH} — a misconfiguration, never a bad token.
77
96
  */
78
97
  export declare function verifyDoubleSubmit(cookieToken: string | undefined, requestToken: string | undefined, secret: string, options?: number | CsrfTokenOptions): boolean;
79
98
  /**
@@ -81,7 +100,8 @@ export declare function verifyDoubleSubmit(cookieToken: string | undefined, requ
81
100
  *
82
101
  * @param method - The HTTP method.
83
102
  * @param config - Optional CSRF configuration.
84
- * @returns True if CSRF protection is required.
103
+ * @returns True if CSRF protection is required. A missing or non-string
104
+ * method is treated as requiring it, so a malformed request fails closed.
85
105
  */
86
106
  export declare function requiresCsrfProtection(method: string, config?: CsrfConfig): boolean;
87
107
  /**
@@ -170,7 +190,8 @@ export interface CsrfProtection {
170
190
  * Verify a request under the double-submit pattern.
171
191
  *
172
192
  * Returns `true` for a method that does not require protection, so it can be
173
- * called unconditionally.
193
+ * called unconditionally, and `false` — never a throw — for a request whose
194
+ * token is missing, malformed, forged, expired or for another session.
174
195
  */
175
196
  verify(request: CsrfVerifiableRequest, options?: {
176
197
  readonly sessionId?: string;
@@ -17,6 +17,22 @@
17
17
  *
18
18
  * Bind the token to a session wherever you have one: without `sessionId`, a
19
19
  * token minted for one user validates for every other user.
20
+ *
21
+ * Error contract: the verifiers (`validateCsrfToken`, `verifyDoubleSubmit`,
22
+ * `CsrfProtection.verify`) return `false` for anything wrong with the
23
+ * *request* — a missing, malformed, non-string, forged, expired or
24
+ * mismatched token, or a request with no method. They throw
25
+ * `ConfigurationError` only for a *configuration* mistake: a secret shorter
26
+ * than {@link MIN_CSRF_SECRET_LENGTH} (32) characters, or an unusable
27
+ * `methods` list. Load the secret from the environment and refuse to start
28
+ * without it; never fall back to a hard-coded string:
29
+ *
30
+ * ```typescript
31
+ * const secret = process.env.CSRF_SECRET;
32
+ * if (!secret || secret.length < MIN_CSRF_SECRET_LENGTH) {
33
+ * throw new Error("Set CSRF_SECRET to at least 32 random characters.");
34
+ * }
35
+ * ```
20
36
  */
21
37
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
22
38
  import { ConfigurationError } from "@zudojs/errors";
@@ -183,11 +199,20 @@ export function validateCsrfToken(token, secret, options) {
183
199
  * @param requestToken - Token taken from the request header or form field.
184
200
  * @param secret - The secret key for verification.
185
201
  * @param options - Maximum lifetime and session binding.
186
- * @returns True when the request carries a matching, valid token.
202
+ * @returns True when the request carries a matching, valid token; `false`
203
+ * for a missing, non-string, mismatched, forged or expired one.
204
+ * @throws {ConfigurationError} only when the secret is empty or shorter than
205
+ * {@link MIN_CSRF_SECRET_LENGTH} — a misconfiguration, never a bad token.
187
206
  */
188
207
  export function verifyDoubleSubmit(cookieToken, requestToken, secret, options) {
189
208
  assertUsableSecret(secret);
190
- if (!cookieToken || !requestToken) {
209
+ // Both values come from the request. A non-string (a parsed-body number, an
210
+ // array from a header bag) used to reach `Buffer.from` and throw a
211
+ // TypeError out of a function whose contract is a boolean.
212
+ if (typeof cookieToken !== "string" ||
213
+ typeof requestToken !== "string" ||
214
+ cookieToken.length === 0 ||
215
+ requestToken.length === 0) {
191
216
  return false;
192
217
  }
193
218
  if (!safeEqual(cookieToken, requestToken)) {
@@ -200,10 +225,14 @@ export function verifyDoubleSubmit(cookieToken, requestToken, secret, options) {
200
225
  *
201
226
  * @param method - The HTTP method.
202
227
  * @param config - Optional CSRF configuration.
203
- * @returns True if CSRF protection is required.
228
+ * @returns True if CSRF protection is required. A missing or non-string
229
+ * method is treated as requiring it, so a malformed request fails closed.
204
230
  */
205
231
  export function requiresCsrfProtection(method, config) {
206
232
  assertUsableMethods(config?.methods);
233
+ if (typeof method !== "string") {
234
+ return true;
235
+ }
207
236
  if (SAFE_METHODS.includes(method.toUpperCase())) {
208
237
  return false;
209
238
  }
@@ -226,6 +255,8 @@ export function requiresCsrfProtection(method, config) {
226
255
  */
227
256
  export function extractCsrfTokenFromHeaders(headers, headerName) {
228
257
  const name = (headerName ?? DEFAULT_HEADER_NAME).toLowerCase();
258
+ if (headers === null || typeof headers !== "object")
259
+ return undefined;
229
260
  let value;
230
261
  for (const key of Object.keys(headers)) {
231
262
  if (key.toLowerCase() === name) {
@@ -236,7 +267,7 @@ export function extractCsrfTokenFromHeaders(headers, headerName) {
236
267
  if (typeof value === "string") {
237
268
  return value;
238
269
  }
239
- if (Array.isArray(value) && value.length > 0) {
270
+ if (Array.isArray(value) && typeof value[0] === "string") {
240
271
  return value[0];
241
272
  }
242
273
  return undefined;
@@ -250,6 +281,8 @@ export function extractCsrfTokenFromHeaders(headers, headerName) {
250
281
  */
251
282
  export function extractCsrfTokenFromCookies(cookieHeader, cookieName) {
252
283
  const name = cookieName ?? DEFAULT_COOKIE_NAME;
284
+ if (typeof cookieHeader !== "string")
285
+ return undefined;
253
286
  const cookies = cookieHeader.split(";").map((pair) => {
254
287
  const eqIndex = pair.indexOf("=");
255
288
  if (eqIndex === -1)
@@ -331,6 +364,9 @@ export function createCsrfProtection(config) {
331
364
  };
332
365
  },
333
366
  verify(request, options) {
367
+ if (request === null || typeof request !== "object") {
368
+ return false;
369
+ }
334
370
  if (!requiresCsrfProtection(request.method, config)) {
335
371
  return true;
336
372
  }
package/dist/index.d.ts CHANGED
@@ -45,5 +45,5 @@ export type { RateLimiterOptions, ClientIpOptions, IpKeyOptions, } from "./rateL
45
45
  export { rateLimit } from "./rateLimit/rateLimit.namespace.js";
46
46
  export { SECURITY_HEADER_NAMES } from "./headers/index.js";
47
47
  export { generateSecurityHeaders, getMissingSecurityHeaders, generateCspNonce, validateCspDirective, } from "./headers/index.js";
48
- export { containsSqlInjection, containsXss, containsPrototypePollution, sanitizeString, sanitizeObject, isSafeString, withoutStickyFlags, detectThreats, escapeHtml, stripHtml, } from "./input/index.js";
48
+ export { containsSqlInjection, containsXss, containsPrototypePollution, sanitizeString, sanitizeObject, isSafeString, withoutStickyFlags, detectThreats, escapeHtml, stripHtml, findUnsafeKey, } from "./input/index.js";
49
49
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -48,5 +48,5 @@ export { rateLimit } from "./rateLimit/rateLimit.namespace.js";
48
48
  export { SECURITY_HEADER_NAMES } from "./headers/index.js";
49
49
  export { generateSecurityHeaders, getMissingSecurityHeaders, generateCspNonce, validateCspDirective, } from "./headers/index.js";
50
50
  /* ─── Input Sanitization ─────────────────────────────────────────────────── */
51
- export { containsSqlInjection, containsXss, containsPrototypePollution, sanitizeString, sanitizeObject, isSafeString, withoutStickyFlags, detectThreats, escapeHtml, stripHtml, } from "./input/index.js";
51
+ export { containsSqlInjection, containsXss, containsPrototypePollution, sanitizeString, sanitizeObject, isSafeString, withoutStickyFlags, detectThreats, escapeHtml, stripHtml, findUnsafeKey, } from "./input/index.js";
52
52
  //# sourceMappingURL=index.js.map
@@ -2,4 +2,5 @@
2
2
  * @zudojs/security — Input Sanitization Barrel
3
3
  */
4
4
  export { containsSqlInjection, containsXss, containsPrototypePollution, sanitizeString, sanitizeObject, isSafeString, withoutStickyFlags, detectThreats, escapeHtml, stripHtml, } from "./input.core.js";
5
+ export { findUnsafeKey } from "./input.unsafeKey.js";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -2,4 +2,5 @@
2
2
  * @zudojs/security — Input Sanitization Barrel
3
3
  */
4
4
  export { containsSqlInjection, containsXss, containsPrototypePollution, sanitizeString, sanitizeObject, isSafeString, withoutStickyFlags, detectThreats, escapeHtml, stripHtml, } from "./input.core.js";
5
+ export { findUnsafeKey } from "./input.unsafeKey.js";
5
6
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Deep detection of prototype-polluting keys in decoded, untrusted data
3
+ * (objects and arrays). `containsPrototypePollution` covers strings.
4
+ */
5
+ /**
6
+ * Returns the first `__proto__`, `constructor` or `prototype` key found
7
+ * anywhere in `value`, or `undefined` when there is none.
8
+ *
9
+ * `JSON.parse` keeps such a key as an ordinary own property, so it reaches
10
+ * handlers intact; the moment one is copied with `Object.assign`, a
11
+ * `for…in` merge or a bracket assignment, it replaces the target's
12
+ * prototype. Transports refuse a frame that carries one rather than
13
+ * silently dropping it.
14
+ *
15
+ * Walks plain objects and arrays only, iteratively (no recursion limit)
16
+ * and cycle-safe, so it is safe on any decoded or in-process value.
17
+ */
18
+ export declare function findUnsafeKey(value: unknown): string | undefined;
19
+ //# sourceMappingURL=input.unsafeKey.d.ts.map
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Deep detection of prototype-polluting keys in decoded, untrusted data
3
+ * (objects and arrays). `containsPrototypePollution` covers strings.
4
+ */
5
+ import { SCHEMA_FORBIDDEN_KEYS } from "@zudojs/constants";
6
+ function isTraversable(value) {
7
+ if (typeof value !== "object" || value === null) {
8
+ return false;
9
+ }
10
+ if (Array.isArray(value)) {
11
+ return true;
12
+ }
13
+ const prototype = Object.getPrototypeOf(value);
14
+ return prototype === Object.prototype || prototype === null;
15
+ }
16
+ /**
17
+ * Returns the first `__proto__`, `constructor` or `prototype` key found
18
+ * anywhere in `value`, or `undefined` when there is none.
19
+ *
20
+ * `JSON.parse` keeps such a key as an ordinary own property, so it reaches
21
+ * handlers intact; the moment one is copied with `Object.assign`, a
22
+ * `for…in` merge or a bracket assignment, it replaces the target's
23
+ * prototype. Transports refuse a frame that carries one rather than
24
+ * silently dropping it.
25
+ *
26
+ * Walks plain objects and arrays only, iteratively (no recursion limit)
27
+ * and cycle-safe, so it is safe on any decoded or in-process value.
28
+ */
29
+ export function findUnsafeKey(value) {
30
+ if (!isTraversable(value)) {
31
+ return undefined;
32
+ }
33
+ const seen = new WeakSet();
34
+ const pending = [value];
35
+ while (pending.length > 0) {
36
+ const node = pending.pop();
37
+ if (seen.has(node)) {
38
+ continue;
39
+ }
40
+ seen.add(node);
41
+ if (Array.isArray(node)) {
42
+ for (const child of node) {
43
+ if (isTraversable(child)) {
44
+ pending.push(child);
45
+ }
46
+ }
47
+ continue;
48
+ }
49
+ for (const key of Object.keys(node)) {
50
+ if (SCHEMA_FORBIDDEN_KEYS.has(key)) {
51
+ return key;
52
+ }
53
+ const child = node[key];
54
+ if (isTraversable(child)) {
55
+ pending.push(child);
56
+ }
57
+ }
58
+ }
59
+ return undefined;
60
+ }
61
+ //# sourceMappingURL=input.unsafeKey.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/security",
3
- "version": "1.2.0",
3
+ "version": "1.3.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,13 +24,13 @@
24
24
  "!dist/.tsbuildinfo"
25
25
  ],
26
26
  "dependencies": {
27
- "@zudojs/errors": "1.2.0",
28
- "@zudojs/constants": "1.1.1"
27
+ "@zudojs/errors": "1.3.0",
28
+ "@zudojs/constants": "1.1.2"
29
29
  },
30
30
  "devDependencies": {
31
31
  "typescript": "7.0.2",
32
- "vitest": "^4.1.11",
33
- "@types/node": "^26.4.1"
32
+ "vitest": "^5.0.1",
33
+ "@types/node": "^26.6.2"
34
34
  },
35
35
  "engines": {
36
36
  "node": ">=24.0.0"
@@ -45,7 +45,7 @@
45
45
  "csrf",
46
46
  "rate-limiting"
47
47
  ],
48
- "homepage": "https://github.com/oyinlola-tech/zudo#readme",
48
+ "homepage": "https://zudojs.oyinlola.site/docs/packages-security",
49
49
  "bugs": {
50
50
  "url": "https://github.com/oyinlola-tech/zudo/issues"
51
51
  },