@zudojs/security 1.0.0 → 1.0.1
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/body/body.core.js
CHANGED
|
@@ -77,13 +77,19 @@ export function validateBodyFraming(headers, maxSize) {
|
|
|
77
77
|
}
|
|
78
78
|
return validateContentLength(contentLength, maxSize);
|
|
79
79
|
}
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
// A repeated Transfer-Encoding field is one comma-separated list (RFC 9110
|
|
81
|
+
// s5.3), so an array is flattened before the final-coding check. It used to
|
|
82
|
+
// be skipped entirely, so `["gzip"]` passed where `"gzip"` was rejected.
|
|
83
|
+
const transferEncodingList = Array.isArray(transferEncoding)
|
|
84
|
+
? transferEncoding.join(",")
|
|
85
|
+
: transferEncoding;
|
|
86
|
+
if (typeof transferEncodingList === "string") {
|
|
87
|
+
const encodings = transferEncodingList
|
|
82
88
|
.toLowerCase()
|
|
83
89
|
.split(",")
|
|
84
90
|
.map((e) => e.trim());
|
|
85
91
|
if (encodings.length > 0 && encodings[encodings.length - 1] !== "chunked") {
|
|
86
|
-
return `Transfer-Encoding must end with "chunked", got: ${
|
|
92
|
+
return `Transfer-Encoding must end with "chunked", got: ${transferEncodingList}`;
|
|
87
93
|
}
|
|
88
94
|
}
|
|
89
95
|
return undefined;
|
|
@@ -9,6 +9,8 @@ const DEFAULT_WINDOW_MS = 60_000;
|
|
|
9
9
|
const DEFAULT_MESSAGE = "Too many requests";
|
|
10
10
|
/** Default cap on tracked keys before least-recently-seen eviction. */
|
|
11
11
|
const DEFAULT_MAX_KEYS = 100_000;
|
|
12
|
+
/** Largest delay `setInterval` honours without overflowing to 1 ms. */
|
|
13
|
+
const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
12
14
|
/**
|
|
13
15
|
* Default key generator using IP address.
|
|
14
16
|
*
|
|
@@ -88,8 +90,11 @@ export function createRateLimiter(config) {
|
|
|
88
90
|
});
|
|
89
91
|
const skip = config.skip;
|
|
90
92
|
const maxKeys = config.maxKeys ?? DEFAULT_MAX_KEYS;
|
|
91
|
-
// Cleanup old entries periodically. Bounded so a very short window
|
|
92
|
-
// schedule a near-continuous timer
|
|
93
|
+
// Cleanup old entries periodically. Bounded below so a very short window
|
|
94
|
+
// does not schedule a near-continuous timer, and above because Node stores
|
|
95
|
+
// timer delays as a signed 32-bit integer: a delay past 2^31 - 1 ms (about
|
|
96
|
+
// 24.8 days) is silently replaced with 1 ms, so a month-long window used to
|
|
97
|
+
// sweep the whole store a thousand times a second.
|
|
93
98
|
const cleanupInterval = setInterval(() => {
|
|
94
99
|
const cutoff = Date.now() - config.windowMs;
|
|
95
100
|
for (const [key, entry] of store) {
|
|
@@ -97,7 +102,7 @@ export function createRateLimiter(config) {
|
|
|
97
102
|
store.delete(key);
|
|
98
103
|
}
|
|
99
104
|
}
|
|
100
|
-
}, Math.max(config.windowMs, 1_000));
|
|
105
|
+
}, Math.min(Math.max(config.windowMs, 1_000), MAX_TIMER_DELAY_MS));
|
|
101
106
|
// Allow cleanup to not keep process alive
|
|
102
107
|
if (cleanupInterval.unref) {
|
|
103
108
|
cleanupInterval.unref();
|
|
@@ -107,16 +112,20 @@ export function createRateLimiter(config) {
|
|
|
107
112
|
*
|
|
108
113
|
* Without this, a caller rotating the key (a spoofed forwarding header, a
|
|
109
114
|
* per-request identifier) grows the map without limit between sweeps.
|
|
115
|
+
*
|
|
116
|
+
* The store is kept in recency order — `check` re-inserts an entry on every
|
|
117
|
+
* hit — so the oldest key is always the first one iterated and eviction is
|
|
118
|
+
* O(1). It used to copy and sort the whole map on every new key past the
|
|
119
|
+
* cap, which turned the defence against key rotation into an O(n log n)
|
|
120
|
+
* cost per rotated request: the attack it was meant to bound became the
|
|
121
|
+
* cheapest way to burn the CPU.
|
|
110
122
|
*/
|
|
111
123
|
function evictIfNeeded() {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const entry = entries[i];
|
|
118
|
-
if (entry)
|
|
119
|
-
store.delete(entry[0]);
|
|
124
|
+
while (store.size > maxKeys) {
|
|
125
|
+
const oldest = store.keys().next();
|
|
126
|
+
if (oldest.done)
|
|
127
|
+
break;
|
|
128
|
+
store.delete(oldest.value);
|
|
120
129
|
}
|
|
121
130
|
}
|
|
122
131
|
/**
|
|
@@ -141,6 +150,11 @@ export function createRateLimiter(config) {
|
|
|
141
150
|
store.set(key, entry);
|
|
142
151
|
evictIfNeeded();
|
|
143
152
|
}
|
|
153
|
+
else {
|
|
154
|
+
// Move to the most-recent end so eviction order stays least-recent-first.
|
|
155
|
+
store.delete(key);
|
|
156
|
+
store.set(key, entry);
|
|
157
|
+
}
|
|
144
158
|
// Prune everything that has slid out of the window.
|
|
145
159
|
const timestamps = entry.timestamps.filter((t) => t > windowStart);
|
|
146
160
|
entry.lastSeen = now;
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zudojs/security",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Security primitives for input validation, header security, CORS, CSRF protection, rate limiting, and security headers.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Oluwayemi Oyinlola",
|
|
8
|
+
"url": "https://github.com/oyinlola-tech"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
7
11
|
"main": "./dist/index.js",
|
|
8
12
|
"module": "./dist/index.js",
|
|
@@ -20,8 +24,8 @@
|
|
|
20
24
|
"!dist/.tsbuildinfo"
|
|
21
25
|
],
|
|
22
26
|
"dependencies": {
|
|
23
|
-
"@zudojs/errors": "1.0.
|
|
24
|
-
"@zudojs/constants": "1.0.
|
|
27
|
+
"@zudojs/errors": "1.0.1",
|
|
28
|
+
"@zudojs/constants": "1.0.1"
|
|
25
29
|
},
|
|
26
30
|
"devDependencies": {
|
|
27
31
|
"typescript": "7.0.2",
|