@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.
@@ -77,13 +77,19 @@ export function validateBodyFraming(headers, maxSize) {
77
77
  }
78
78
  return validateContentLength(contentLength, maxSize);
79
79
  }
80
- if (typeof transferEncoding === "string") {
81
- const encodings = transferEncoding
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: ${transferEncoding}`;
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 does not
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
- if (store.size <= maxKeys)
113
- return;
114
- const entries = [...store.entries()].sort((a, b) => a[1].lastSeen - b[1].lastSeen);
115
- const excess = store.size - maxKeys;
116
- for (let i = 0; i < excess; i++) {
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.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.0",
24
- "@zudojs/constants": "1.0.0"
27
+ "@zudojs/errors": "1.0.1",
28
+ "@zudojs/constants": "1.0.1"
25
29
  },
26
30
  "devDependencies": {
27
31
  "typescript": "7.0.2",