Skip to content

Commit f9255e9

Browse files
authored
Security Review 07/02/2026 (#13)
Resolves multiple security vulnerabilities from a penetration test—including hardening the body-parser against prototype pollution, preventing CORS policy header leakage to rejected origins, improving rate limiter IP detection, adding refresh-on-access to the LRU cache—along with comprehensive test coverage.
1 parent f808fba commit f9255e9

26 files changed

+2263
-614
lines changed

README.md

Lines changed: 304 additions & 51 deletions
Large diffs are not rendered by default.

SECURITY_REVIEWS.md

Lines changed: 361 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,361 @@
1+
# Security Remediation TODO
2+
3+
> Penetration test conducted on 2025-02-07 against `0http-bun@1.2.2`.
4+
> **43 vulnerabilities found** — 6 Critical, 13 High, 13 Medium, 7 Low, 4 Info.
5+
> **Overall Security Grade: D → B+ — All identified vulnerabilities resolved.**
6+
7+
## Remediation Progress
8+
9+
> **Fixed:** 6/6 Critical, 13/13 High, 13/13 Medium, 7/7 Low, 4/4 Info = **43/43 vulnerabilities resolved**
10+
11+
---
12+
13+
## CRITICAL (Must fix immediately)
14+
15+
### ✅ CRIT-1: Unbounded Route Cache — Memory Exhaustion DoS
16+
17+
- **Status:** FIXED
18+
- **File:** `lib/router/sequential.js`
19+
- **Fix applied:** Added LRU-style cache eviction with configurable `config.cacheSize` (default 1000). When cache exceeds max size, oldest entries are evicted.
20+
21+
---
22+
23+
### ✅ CRIT-2: JSON Nesting Depth Check Bypass via String Content
24+
25+
- **Status:** FIXED
26+
- **File:** `lib/middleware/body-parser.js`
27+
- **Fix applied:** Nesting depth scanner now tracks `inString`/`escape` state to correctly ignore braces inside JSON string literals.
28+
29+
---
30+
31+
### ✅ CRIT-3: Rate Limit Bypass via Spoofable IP Headers
32+
33+
- **Status:** FIXED — ⚠️ BREAKING CHANGE
34+
- **File:** `lib/middleware/rate-limit.js`
35+
- **Fix applied:** `defaultKeyGenerator` now uses `req.ip || req.remoteAddress || 'unknown'` — no proxy headers trusted by default. Users behind reverse proxies must supply a custom `keyGenerator`.
36+
37+
---
38+
39+
### ✅ CRIT-4: Rate Limit Store Injection via `req.rateLimitStore`
40+
41+
- **Status:** FIXED
42+
- **File:** `lib/middleware/rate-limit.js`
43+
- **Fix applied:** Removed `req.rateLimitStore` backdoor. Always uses constructor-configured store.
44+
45+
---
46+
47+
### ✅ CRIT-5: API Key Timing Side-Channel Attack
48+
49+
- **Status:** FIXED
50+
- **File:** `lib/middleware/jwt-auth.js`
51+
- **Fix applied:** Added `crypto.timingSafeEqual()` for all API key comparisons with constant-time length mismatch handling.
52+
53+
---
54+
55+
### ✅ CRIT-6: JWT Default Algorithms Enable Algorithm Confusion
56+
57+
- **Status:** FIXED — ⚠️ BREAKING CHANGE
58+
- **File:** `lib/middleware/jwt-auth.js`
59+
- **Fix applied:** Default algorithms changed from `['HS256', 'RS256']` to `['HS256']`. Throws error if mixed symmetric+asymmetric algorithms are configured.
60+
61+
---
62+
63+
## HIGH (Must fix before next release)
64+
65+
### ✅ H-1: Default Error Handler Leaks `err.message` to Clients
66+
67+
- **Status:** FIXED
68+
- **File:** `lib/router/sequential.js`
69+
- **Fix applied:** Default error handler now returns generic `"Internal Server Error"` and logs full error via `console.error`.
70+
71+
---
72+
73+
### ✅ H-2: `this` Binding Bug in `router.use()` Breaks Middleware Chaining
74+
75+
- **Status:** FIXED
76+
- **File:** `lib/router/sequential.js`
77+
- **Fix applied:** Changed `return this``return router` in arrow function `router.use()`.
78+
79+
---
80+
81+
### ✅ H-3: Content-Length Bypass — Full Body Read Before Size Enforcement
82+
83+
- **Status:** FIXED
84+
- **File:** `lib/middleware/body-parser.js`
85+
- **Fix applied:** Added `readBodyWithLimit()` streaming helper that reads body chunks incrementally and aborts immediately when cumulative size exceeds the limit. Replaces `await req.text()` + post-hoc size check in JSON, text, URL-encoded, and custom JSON parser paths.
86+
87+
---
88+
89+
### ✅ H-4: Custom `jsonParser` Bypasses All Security Controls
90+
91+
- **Status:** FIXED
92+
- **File:** `lib/middleware/body-parser.js`
93+
- **Fix applied:** Custom `jsonParser` now enforces size limits before calling the custom parser function.
94+
95+
---
96+
97+
### ✅ H-5: Multipart Parser Reads Entire Body Before Validation + Double Memory
98+
99+
- **Status:** FIXED
100+
- **File:** `lib/middleware/body-parser.js`
101+
- **Fix applied:** Merged the validation and extraction loops into a single pass. Field count, size limits, filename sanitization, and prototype pollution checks all run in one iteration of `formData.entries()` instead of two.
102+
103+
---
104+
105+
### ✅ H-6: Multipart Parser Has No Prototype Pollution Protection
106+
107+
- **Status:** FIXED
108+
- **File:** `lib/middleware/body-parser.js`
109+
- **Fix applied:** Uses `Object.create(null)` for body/files objects and added prototype pollution blocklist for field names.
110+
111+
---
112+
113+
### ✅ H-7: JWT Path Exclusion Bypass via Prefix Collision
114+
115+
- **Status:** FIXED
116+
- **File:** `lib/middleware/jwt-auth.js`
117+
- **Fix applied:** Path exclusion now uses exact match or `path + '/'` boundary checking.
118+
119+
---
120+
121+
### ✅ H-8: JWT Token in Query Parameters Leaks via Logs/Referrer
122+
123+
- **Status:** FIXED
124+
- **File:** `lib/middleware/jwt-auth.js`
125+
- **Fix applied:** Emits a `console.warn` deprecation warning at middleware construction time when `tokenQuery` is configured, advising migration to Authorization header or custom `getToken`.
126+
127+
---
128+
129+
### ✅ H-9: Optional JWT Mode Silently Swallows Invalid/Forged Tokens
130+
131+
- **Status:** FIXED
132+
- **File:** `lib/middleware/jwt-auth.js`
133+
- **Fix applied:** Now sets `req.ctx.authError` and `req.ctx.authAttempted = true` on invalid tokens in optional mode.
134+
135+
---
136+
137+
### ✅ H-10: Sliding Window Rate Limiter — Unbounded Map Growth
138+
139+
- **Status:** FIXED
140+
- **File:** `lib/middleware/rate-limit.js`
141+
- **Fix applied:** Added `maxKeys` (default 10000), periodic cleanup with `setInterval` + `unref()`, and eviction of stale entries.
142+
143+
---
144+
145+
### ✅ H-11: CORS `null` Origin Bypass via Sandboxed Iframe
146+
147+
- **Status:** FIXED
148+
- **File:** `lib/middleware/cors.js`
149+
- **Fix applied:** `null`/missing origins are now rejected before calling validator function or checking array.
150+
151+
---
152+
153+
### ✅ H-12: Rate Limiter TOCTOU Race Condition
154+
155+
- **Status:** FIXED
156+
- **File:** `lib/middleware/rate-limit.js`
157+
- **Fix applied:** `MemoryStore.increment` changed from `async` to synchronous.
158+
159+
---
160+
161+
### ✅ H-13: Async Errors Bypass Custom Error Handler
162+
163+
- **Status:** FIXED
164+
- **File:** `lib/next.js`
165+
- **Fix applied:** Async middleware errors now caught via `.catch()` on returned promises and forwarded to `errorHandler`.
166+
167+
---
168+
169+
## MEDIUM (Should fix soon)
170+
171+
### ✅ M-1: Shared Mutable `emptyParams` — Cross-Request Data Leakage
172+
173+
- **Status:** FIXED
174+
- **File:** `lib/router/sequential.js`
175+
- **Fix applied:** `emptyParams` now uses `Object.freeze({})`.
176+
177+
---
178+
179+
### ✅ M-2: No URL Path Normalization — Route Filter Bypass
180+
181+
- **Status:** FIXED
182+
- **File:** `lib/router/sequential.js`
183+
- **Fix applied:** Added URL path normalization (double-slash collapse, `decodeURIComponent`, preserves `%2F`).
184+
185+
---
186+
187+
### ✅ M-3: Multipart Filename Not Sanitized for Path Traversal
188+
189+
- **Status:** FIXED
190+
- **File:** `lib/middleware/body-parser.js`
191+
- **Fix applied:** Filenames sanitized (strips `..`, path separators, null bytes; preserves `originalName`).
192+
193+
---
194+
195+
### ✅ M-4: Content-Type Broad Matching Routes Non-JSON to JSON Parser
196+
197+
- **Status:** FIXED
198+
- **File:** `lib/middleware/body-parser.js`
199+
- **Fix applied:** Internal JSON parser type changed from `'application/'` to `'application/json'`.
200+
201+
---
202+
203+
### ✅ M-5: `parseLimit` Silently Defaults for Unexpected Types
204+
205+
- **Status:** FIXED — ⚠️ BREAKING CHANGE
206+
- **File:** `lib/middleware/body-parser.js`
207+
- **Fix applied:** Now throws `TypeError` on unexpected types instead of silently defaulting.
208+
209+
---
210+
211+
### ✅ M-6: Raw JWT Token Stored on Request Context
212+
213+
- **Status:** FIXED
214+
- **File:** `lib/middleware/jwt-auth.js`
215+
- **Fix applied:** Raw JWT token removed from `req.ctx.jwt` and `req.jwt`.
216+
217+
---
218+
219+
### ✅ M-7: API Key Stored in Plain Text on Request Context
220+
221+
- **Status:** FIXED — ⚠️ BREAKING CHANGE
222+
- **File:** `lib/middleware/jwt-auth.js`
223+
- **Fix applied:** `req.ctx.apiKey` and `req.apiKey` now store a masked version (`xxxx****xxxx`). Raw key is available only via `req[API_KEY_SYMBOL]` (exported `Symbol.for('0http.apiKey')`) to prevent accidental serialization/logging.
224+
225+
---
226+
227+
### ✅ M-8: JWKS URI Not Validated (Allows HTTP)
228+
229+
- **Status:** FIXED
230+
- **File:** `lib/middleware/jwt-auth.js`
231+
- **Fix applied:** JWKS URI is validated at construction time. Throws an error if non-HTTPS URI is used when `NODE_ENV=production`. Emits a `console.warn` in non-production environments.
232+
233+
---
234+
235+
### ✅ M-9: JWT Error Messages Reveal Validation Oracle
236+
237+
- **Status:** FIXED
238+
- **File:** `lib/middleware/jwt-auth.js`
239+
- **Fix applied:** Unified JWT error messages to `'Invalid or expired token'`.
240+
241+
---
242+
243+
### ✅ M-10: `jwtOptions` Spread Overrides Security-Critical Defaults
244+
245+
- **Status:** FIXED
246+
- **File:** `lib/middleware/jwt-auth.js`
247+
- **Fix applied:** `...jwtOptions` spread now applied first, security-critical options override after.
248+
249+
---
250+
251+
### ✅ M-11: Rate Limit `excludePaths` Uses Prefix Matching
252+
253+
- **Status:** FIXED
254+
- **File:** `lib/middleware/rate-limit.js`
255+
- **Fix applied:** `excludePaths` uses exact or boundary matching.
256+
257+
---
258+
259+
### ✅ M-12: CORS Headers Leak Method/Header Lists When Origin Rejected
260+
261+
- **Status:** FIXED
262+
- **File:** `lib/middleware/cors.js`
263+
- **Fix applied:** CORS headers (methods, allowed headers, credentials, exposed headers) only set when origin is allowed.
264+
265+
---
266+
267+
### ✅ M-13: `apiKeyValidator.length` Arity Check Is Unreliable
268+
269+
- **Status:** FIXED
270+
- **File:** `lib/middleware/jwt-auth.js`
271+
- **Fix applied:** Removed unreliable `Function.length` arity check; always calls validators with `(apiKey, req)`.
272+
273+
---
274+
275+
## LOW (Consider fixing)
276+
277+
### ✅ L-1: `fast-querystring` `__proto__` in Query Can Pollute Downstream
278+
279+
- **Status:** FIXED
280+
- **File:** `lib/router/sequential.js`
281+
- **Fix applied:** After parsing query string, dangerous keys (`__proto__`, `constructor`, `prototype`) are deleted from `req.query`, mirroring the existing protection on `req.params`.
282+
283+
### ✅ L-2: Empty JSON Body Silently Becomes `{}`
284+
285+
- **Status:** FIXED — ⚠️ BREAKING CHANGE
286+
- **File:** `lib/middleware/body-parser.js`
287+
- **Fix applied:** Empty or whitespace-only JSON bodies now set `req.body` to `undefined` instead of `{}`, letting the application decide the semantics.
288+
289+
### ✅ L-3: `_rawBodyText` Publicly Accessible
290+
291+
- **Status:** FIXED
292+
- **File:** `lib/middleware/body-parser.js`
293+
- **Fix applied:** Raw body text now stored via `Symbol.for('0http.rawBody')` (`RAW_BODY_SYMBOL`) instead of `req._rawBodyText`, preventing accidental serialization/logging. Symbol is exported for programmatic access.
294+
295+
### ✅ L-4: Missing JWT Token Type (`typ`) Header Validation
296+
297+
- **Status:** FIXED
298+
- **File:** `lib/middleware/jwt-auth.js`
299+
- **Fix applied:** Added `requiredTokenType` option to `createJWTAuth`. After successful JWT verification, validates the `typ` header claim (case-insensitive). Rejects tokens with missing or incorrect type when configured.
300+
301+
### ✅ L-5: Internal Functions Exported, Expanding Attack Surface
302+
303+
- **Status:** FIXED
304+
- **File:** `lib/middleware/jwt-auth.js`
305+
- **Fix applied:** Internal functions removed from module exports.
306+
307+
### ✅ L-6: Rate Limit Headers Disclose Exact Config/Counters
308+
309+
- **Status:** FIXED
310+
- **File:** `lib/middleware/rate-limit.js`
311+
- **Fix applied:** `standardHeaders` now accepts `true` (full headers — default, backward compatible), `false` (no headers), or `'minimal'` (only `Retry-After` on 429 responses). Minimal mode prevents disclosing exact rate limit configuration and usage counters.
312+
313+
### ✅ L-7: Missing `Vary: Origin` for Static String CORS Origins
314+
315+
- **Status:** FIXED
316+
- **File:** `lib/middleware/cors.js`
317+
- **Fix applied:** `Vary: Origin` now set for all non-wildcard origins (not just function/array).
318+
319+
---
320+
321+
## INFO (Awareness)
322+
323+
-**I-1:** `'unknown'` fallback in rate limiter now generates unique per-request keys to prevent shared bucket DoS; logs a security warning on first occurrence (`rate-limit.js`) — FIXED
324+
-**I-2:** `allowedHeaders` function now resolved once per preflight request instead of 3 times, preventing inconsistency and wasted resources (`cors.js`) — FIXED
325+
-**I-3:** Pointless `try/catch` blocks that only re-throw in body parser — FIXED (removed)
326+
-**I-4:** Empty `catch` blocks in JWT `handleAuthError` now log errors via `console.error` to aid debugging (`jwt-auth.js`) — FIXED
327+
328+
---
329+
330+
## Positive Observations
331+
332+
- Prototype pollution protection on route params is well-implemented with `hasOwnProperty.call()` guard
333+
- `fast-querystring` uses null-prototype objects for query results
334+
- Security test suite exists for prototype pollution scenarios
335+
- Body parser has comprehensive limits (parameter counts, field counts, key lengths)
336+
- JWT error messages are partially sanitized (enumerated messages, not raw errors)
337+
- CORS correctly blocks `credentials: true` + `origin: '*'` combination
338+
- Lazy loading of heavy dependencies reduces startup attack surface
339+
- `parseLimit` regex is anchored and bounded (not vulnerable to ReDoS)
340+
341+
---
342+
343+
## Breaking Changes Summary
344+
345+
| Vulnerability | Breaking Change | Impact |
346+
| ------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------- |
347+
| CRIT-3 | Default rate limit key generator no longer trusts proxy headers | Users behind reverse proxies must supply custom `keyGenerator` |
348+
| CRIT-6 | Default JWT algorithms changed from `['HS256', 'RS256']` to `['HS256']` | Users relying on RS256 must explicitly configure algorithms |
349+
| M-5 | `parseLimit` throws `TypeError` on invalid types | Code passing `false`, `null`, or objects to `parseLimit` will throw |
350+
| M-7 | `req.ctx.apiKey` / `req.apiKey` now stores masked value | Code reading raw API key must use `req[API_KEY_SYMBOL]` instead |
351+
| L-2 | Empty JSON body now sets `req.body` to `undefined` instead of `{}` | Code checking `req.body` for empty JSON must handle `undefined` |
352+
353+
---
354+
355+
## Suggested Next Steps
356+
357+
1. ~~**Fix remaining HIGH issues:** H-3 (streaming body read), H-5 (single-pass multipart), H-8 (query token deprecation)~~ ✅ All HIGH issues fixed
358+
2. ~~**Fix remaining MEDIUM issues:** M-7 (mask API key in context), M-8 (JWKS HTTPS validation)~~ ✅ All MEDIUM issues fixed
359+
3. ~~**Address LOW/INFO issues** as part of ongoing maintenance~~ ✅ All LOW and INFO issues fixed
360+
4. **Write security regression tests** for each fix ✅ Completed — 483 tests passing
361+
5. **Bump version to 1.3.0** with security changelog

0 commit comments

Comments
 (0)