@zudojs/security 1.1.0 → 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/dist/csrf/csrf.core.d.ts +2 -1
- package/dist/csrf/csrf.core.js +31 -1
- package/dist/input/input.core.d.ts +2 -0
- package/dist/input/input.core.js +24 -0
- package/dist/rateLimit/rateLimit.clientIp.js +12 -4
- package/dist/url/url.core.d.ts +6 -0
- package/dist/url/url.core.js +17 -3
- package/package.json +3 -3
package/dist/csrf/csrf.core.d.ts
CHANGED
|
@@ -193,7 +193,8 @@ export interface CsrfProtection {
|
|
|
193
193
|
* @param config - Secret, lifetime, cookie/header names, protected methods.
|
|
194
194
|
* @returns Protection bound to that configuration.
|
|
195
195
|
* @throws {ConfigurationError} when the secret is missing or shorter than
|
|
196
|
-
* {@link MIN_CSRF_SECRET_LENGTH}
|
|
196
|
+
* {@link MIN_CSRF_SECRET_LENGTH}, or when `methods` is present but empty,
|
|
197
|
+
* not an array, or contains a non-method entry.
|
|
197
198
|
*/
|
|
198
199
|
export declare function createCsrfProtection(config: CsrfProtectionOptions): CsrfProtection;
|
|
199
200
|
//# sourceMappingURL=csrf.core.d.ts.map
|
package/dist/csrf/csrf.core.js
CHANGED
|
@@ -38,6 +38,33 @@ const DEFAULT_METHODS = ["POST", "PUT", "PATCH", "DELETE"];
|
|
|
38
38
|
* the only check, which accepted a one-character secret in silence.
|
|
39
39
|
*/
|
|
40
40
|
export const MIN_CSRF_SECRET_LENGTH = 32;
|
|
41
|
+
/**
|
|
42
|
+
* Rejects a `methods` list that cannot protect anything.
|
|
43
|
+
*
|
|
44
|
+
* `methods.some(…)` over an empty list is always `false`, and `verify()`
|
|
45
|
+
* answers `true` for a method that needs no protection — so `methods: []`
|
|
46
|
+
* turned CSRF off for every request without an error or a warning. A list
|
|
47
|
+
* built from configuration (`process.env.CSRF_METHODS?.split(",") ?? []`) is
|
|
48
|
+
* empty exactly when the configuration is missing. An empty list is a
|
|
49
|
+
* configuration error, not a blanket grant; omit `methods` for the defaults.
|
|
50
|
+
*/
|
|
51
|
+
function assertUsableMethods(methods) {
|
|
52
|
+
if (methods === undefined)
|
|
53
|
+
return;
|
|
54
|
+
if (!Array.isArray(methods)) {
|
|
55
|
+
throw new ConfigurationError("CSRF methods must be an array of HTTP method names, " +
|
|
56
|
+
"or omitted for the defaults (POST, PUT, PATCH, DELETE).");
|
|
57
|
+
}
|
|
58
|
+
if (methods.length === 0) {
|
|
59
|
+
throw new ConfigurationError("CSRF methods cannot be empty: an empty list protects no request at " +
|
|
60
|
+
"all. Omit `methods` for the defaults (POST, PUT, PATCH, DELETE).");
|
|
61
|
+
}
|
|
62
|
+
for (const method of methods) {
|
|
63
|
+
if (typeof method !== "string" || method.trim().length === 0) {
|
|
64
|
+
throw new ConfigurationError("CSRF methods must be non-empty HTTP method names.");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
41
68
|
/** Rejects a secret too short to be worth signing with. */
|
|
42
69
|
function assertUsableSecret(secret) {
|
|
43
70
|
if (typeof secret !== "string" || secret.length === 0) {
|
|
@@ -176,6 +203,7 @@ export function verifyDoubleSubmit(cookieToken, requestToken, secret, options) {
|
|
|
176
203
|
* @returns True if CSRF protection is required.
|
|
177
204
|
*/
|
|
178
205
|
export function requiresCsrfProtection(method, config) {
|
|
206
|
+
assertUsableMethods(config?.methods);
|
|
179
207
|
if (SAFE_METHODS.includes(method.toUpperCase())) {
|
|
180
208
|
return false;
|
|
181
209
|
}
|
|
@@ -272,10 +300,12 @@ export function generateCsrfCookie(token, config) {
|
|
|
272
300
|
* @param config - Secret, lifetime, cookie/header names, protected methods.
|
|
273
301
|
* @returns Protection bound to that configuration.
|
|
274
302
|
* @throws {ConfigurationError} when the secret is missing or shorter than
|
|
275
|
-
* {@link MIN_CSRF_SECRET_LENGTH}
|
|
303
|
+
* {@link MIN_CSRF_SECRET_LENGTH}, or when `methods` is present but empty,
|
|
304
|
+
* not an array, or contains a non-method entry.
|
|
276
305
|
*/
|
|
277
306
|
export function createCsrfProtection(config) {
|
|
278
307
|
assertUsableSecret(config.secret);
|
|
308
|
+
assertUsableMethods(config.methods);
|
|
279
309
|
const expiration = config.expiration ?? DEFAULT_EXPIRATION;
|
|
280
310
|
const cookieName = config.cookieName ?? DEFAULT_COOKIE_NAME;
|
|
281
311
|
const headerName = config.headerName ?? DEFAULT_HEADER_NAME;
|
|
@@ -46,6 +46,8 @@ export declare function sanitizeString(input: string, config?: InputSanitization
|
|
|
46
46
|
* @param obj - The object to sanitize.
|
|
47
47
|
* @param config - Optional sanitization configuration.
|
|
48
48
|
* @returns The sanitized object.
|
|
49
|
+
* @throws {ConfigurationError} when `config.maxDepth` is present but is not
|
|
50
|
+
* an integer of 1 or more.
|
|
49
51
|
*/
|
|
50
52
|
export declare function sanitizeObject<T extends Record<string, unknown>>(obj: T, config?: InputSanitizationConfig): T;
|
|
51
53
|
/**
|
package/dist/input/input.core.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Sanitizes user input against common attack patterns.
|
|
5
5
|
*/
|
|
6
|
+
import { ConfigurationError } from "@zudojs/errors";
|
|
6
7
|
import { PROTOTYPE_POLLUTION_KEYS, SQL_INJECTION_PATTERNS, XSS_PATTERNS, } from "../types/security.type.js";
|
|
7
8
|
import { containsObfuscatedScheme, decodeHtmlEntities } from "./input.decode.js";
|
|
8
9
|
/**
|
|
@@ -164,11 +165,34 @@ function isPlainObject(value) {
|
|
|
164
165
|
* @param obj - The object to sanitize.
|
|
165
166
|
* @param config - Optional sanitization configuration.
|
|
166
167
|
* @returns The sanitized object.
|
|
168
|
+
* @throws {ConfigurationError} when `config.maxDepth` is present but is not
|
|
169
|
+
* an integer of 1 or more.
|
|
167
170
|
*/
|
|
168
171
|
export function sanitizeObject(obj, config) {
|
|
169
172
|
const maxDepth = config?.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
173
|
+
assertUsableMaxDepth(config?.maxDepth);
|
|
170
174
|
return sanitizeValue(obj, config, new WeakSet(), 0, maxDepth);
|
|
171
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Rejects a `maxDepth` that cannot describe a depth the caller wants kept.
|
|
178
|
+
*
|
|
179
|
+
* The guard runs before the object is entered, so `maxDepth: 0` discarded the
|
|
180
|
+
* argument itself and returned `undefined` under a non-optional `T` — every
|
|
181
|
+
* field read off the result then threw at a call site TypeScript had told was
|
|
182
|
+
* safe. `Number(process.env.MAX_DEPTH)` with the variable unset is the same
|
|
183
|
+
* shape of mistake as the body limit's `NaN`.
|
|
184
|
+
*
|
|
185
|
+
* @throws {ConfigurationError} when `maxDepth` is present but is not an
|
|
186
|
+
* integer of 1 or more.
|
|
187
|
+
*/
|
|
188
|
+
function assertUsableMaxDepth(maxDepth) {
|
|
189
|
+
if (maxDepth === undefined)
|
|
190
|
+
return;
|
|
191
|
+
if (!Number.isInteger(maxDepth) || maxDepth < 1) {
|
|
192
|
+
throw new ConfigurationError("Input sanitization maxDepth must be an integer of 1 or more, got: " +
|
|
193
|
+
String(maxDepth));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
172
196
|
/**
|
|
173
197
|
* Validates that a string contains only safe characters.
|
|
174
198
|
*
|
|
@@ -36,10 +36,18 @@ export function extractClientIp(headers, options) {
|
|
|
36
36
|
// Walk in from the right: index 0 from the end is the address our own
|
|
37
37
|
// outermost proxy observed, and each additional trusted hop steps left.
|
|
38
38
|
const index = chain.length - trustProxy;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
// A chain shorter than the configured hop count did not pass through all
|
|
40
|
+
// of the proxies whose entries make it trustworthy — a request entering
|
|
41
|
+
// at an inner hop, or one a client shortened on purpose. Clamping the
|
|
42
|
+
// index to 0 landed on the entry the client wrote, handing them their own
|
|
43
|
+
// rate-limit bucket; there is nothing trustworthy to read here, so the
|
|
44
|
+
// header is skipped entirely.
|
|
45
|
+
if (index >= 0) {
|
|
46
|
+
const candidate = chain[index];
|
|
47
|
+
const ip = candidate === undefined ? undefined : parseClientIp(candidate);
|
|
48
|
+
if (ip !== undefined)
|
|
49
|
+
return ip;
|
|
50
|
+
}
|
|
43
51
|
}
|
|
44
52
|
const realIp = lookupHeader(headers, "x-real-ip");
|
|
45
53
|
const ip = realIp === undefined ? undefined : parseClientIp(realIp);
|
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
|
@@ -97,6 +97,12 @@ function decodeOnce(value) {
|
|
|
97
97
|
* Operates on the fully decoded form, and treats a backslash as a separator
|
|
98
98
|
* because Windows and some proxies do.
|
|
99
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
|
+
*
|
|
100
106
|
* @param path - The path to inspect.
|
|
101
107
|
* @returns True when a `..` segment is present.
|
|
102
108
|
*/
|
|
@@ -104,9 +110,17 @@ export function containsTraversal(path) {
|
|
|
104
110
|
const { decoded, truncated } = fullyDecodeUri(path);
|
|
105
111
|
if (truncated)
|
|
106
112
|
return true;
|
|
107
|
-
return decoded
|
|
108
|
-
|
|
109
|
-
|
|
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 === "...";
|
|
110
124
|
}
|
|
111
125
|
/**
|
|
112
126
|
* Validates a URL against security configuration.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/security",
|
|
3
|
-
"version": "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.
|
|
28
|
-
"@zudojs/constants": "1.1.
|
|
27
|
+
"@zudojs/errors": "1.2.0",
|
|
28
|
+
"@zudojs/constants": "1.1.1"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"typescript": "7.0.2",
|