Skip to content

Commit 407486d

Browse files
authored
security: red-team audit hardening (#7)
* security: red-team audit fixes across gateway, proxy, LB, JWT, TLS, cluster, ops - Harden client IP extraction against X-Forwarded-For spoofing - Sanitize upstream proxy headers (hop-by-hop, auth, X-Forwarded-*) - Enforce JWT exp claim and minimum HMAC key length - Secure load balancer health checks and sticky sessions - Remove committed TLS private key from working tree; add .gitignore rules - Prevent prototype pollution via safe config merging - Apply TLS hardening options to Bun.serve - Update CI and docs with security hardening - Align tests with new secure defaults; 802/802 tests pass * docs: align API docs with security-hardened public surface + VProtocol certificate - Update ClusterConfig reference with allowedEnvVars and workerScriptAllowlist - Update HealthCheckConfig reference with failure/success thresholds, allowedSchemes, allowedHosts, method, expectedBody - Fix LOAD_BALANCING.md threshold names and add restrict-targets best practice
1 parent ef05383 commit 407486d

51 files changed

Lines changed: 3250 additions & 1015 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
bun-version: latest
1313

1414
- name: Install dependencies
15-
run: bun install
15+
run: bun install --frozen-lockfile
1616

1717
#- name: Linting
1818
# run: bun run format

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,4 +145,9 @@ results/
145145

146146
.plan.md
147147

148-
sec-findings.md
148+
sec-findings.md
149+
sec-findings.md*.pem
150+
VERIFICATION.md
151+
verification-certificate.json
152+
*.key
153+
examples/cert.*

AGENTS.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ src/
8181
### Path Validation (input-validator.ts + utils.ts)
8282

8383
**Two-pass validation** against double-encoding attacks:
84+
8485
1. **First pass:** Check raw path against `blockedPatterns` (catches null bytes, `../`)
8586
2. **Recursive decode:** `recursiveDecodeURIComponent()` decodes up to 5 layers until stable
8687
3. **Second pass:** Check fully-decoded path (catches `%252f``%2f``/`)
@@ -90,6 +91,7 @@ Never validate before decoding — attackers hide behind encoding layers.
9091
### Health Checks (http-load-balancer.ts)
9192

9293
Threshold-based to prevent flapping and cascade failures:
94+
9395
- `failureThreshold` (default 3): consecutive failures before marking unhealthy
9496
- `successThreshold` (default 2): consecutive successes before marking healthy again
9597
- `minHealthyTargets` (default 1): floor check — refuses to mark the last healthy target down
@@ -105,14 +107,23 @@ secure header priority (`cf-connecting-ip` > `x-real-ip`).
105107
### Error Handling
106108

107109
Global error handler registered on the 0http-bun router. In production:
110+
108111
- Returns sanitized `{"error":"Internal server error"}` with status 500
109112
- Never leaks stack traces or internal file paths
110113
- Logs full error details internally
111114

112115
### Blocked Patterns (config.ts)
113116

114117
```typescript
115-
blockedPatterns: [/\.\./, /%2e%2e/i, /%2f/i, /%5c/i, /%00/, /\0/, /%25%32%[fF]/i]
118+
blockedPatterns: [
119+
/\.\./,
120+
/%2e%2e/i,
121+
/%2f/i,
122+
/%5c/i,
123+
/%00/,
124+
/\0/,
125+
/%25%32%[fF]/i,
126+
]
116127
```
117128

118129
Covers: raw `..`, encoded `../`, encoded `/`, encoded `\`, null byte, double-encoded `/`.

benchmark/bungate-gateway.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
/**
2+
* ═══════════════════════════════════════════════════════════════════════════
3+
* FOR BENCHMARKING ONLY — NOT FOR PRODUCTION
4+
* This file intentionally disables security features to maximize throughput.
5+
* Do not use this configuration as a template for production deployments.
6+
* ═══════════════════════════════════════════════════════════════════════════
7+
*/
8+
19
import { BunGateway } from './src'
210
import { BunGateLogger } from './src'
311
import { cpus } from 'os'

benchmark/echo-server-simple.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
1+
/**
2+
* ═══════════════════════════════════════════════════════════════════════════
3+
* FOR BENCHMARKING ONLY — NOT FOR PRODUCTION
4+
* ═══════════════════════════════════════════════════════════════════════════
5+
*/
6+
17
const serverId = process.env.SERVER_ID || 'unknown'
28
const port = parseInt(process.env.SERVER_PORT || '8080')
39

410
let requestCount = 0
511
let startTime = Date.now()
612

13+
const SENSITIVE_HEADERS = new Set([
14+
'authorization',
15+
'cookie',
16+
'proxy-authorization',
17+
'x-api-key',
18+
'x-metrics-key',
19+
])
20+
721
const server = Bun.serve({
822
port,
923
fetch(req) {
@@ -19,24 +33,28 @@ const server = Bun.serve({
1933
})
2034
}
2135

22-
// Echo endpoint with server info
36+
// Echo endpoint with server info — redact sensitive headers
37+
const safeHeaders: Record<string, string> = {}
38+
for (const [name, value] of req.headers.entries()) {
39+
safeHeaders[name] = SENSITIVE_HEADERS.has(name.toLowerCase())
40+
? '[REDACTED]'
41+
: value
42+
}
43+
2344
const response = {
2445
server_id: serverId,
2546
request_count: requestCount,
2647
uptime_ms: now - startTime,
2748
timestamp: now,
2849
method: req.method,
2950
url: req.url,
30-
headers: Object.fromEntries(req.headers.entries()),
51+
headers: safeHeaders,
3152
remote_addr: req.headers.get('x-forwarded-for') || 'unknown',
3253
}
3354

3455
return new Response(JSON.stringify(response, null, 2), {
3556
headers: {
3657
'Content-Type': 'application/json',
37-
Server: `echo-server-${serverId}`,
38-
'X-Request-Count': requestCount.toString(),
39-
'X-Server-Id': serverId,
4058
},
4159
})
4260
},

bun.lock

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/API_REFERENCE.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ interface ClusterConfig {
115115
respawnThresholdTime?: number // Default: 60000ms
116116
shutdownTimeout?: number // Default: 30000ms
117117
exitOnShutdown?: boolean // Default: true
118+
allowedEnvVars?: string[] // Allow-list of env vars forwarded to workers
119+
workerScriptAllowlist?: string[] // Allow-list of worker script paths
118120
}
119121
```
120122

@@ -392,12 +394,17 @@ gateway.addRoute({
392394
```typescript
393395
interface HealthCheckConfig {
394396
enabled: boolean
395-
interval?: number // Default: 30000ms
396-
timeout?: number // Default: 5000ms
397+
interval?: number // Default: 30000ms, clamped to 1000ms–300000ms
398+
timeout?: number // Default: 5000ms, must be less than interval
397399
path?: string // Default: '/health'
400+
method?: 'GET' | 'HEAD' // Default: 'GET'
398401
expectedStatus?: number // Default: 200
399-
unhealthyThreshold?: number // Default: 3
400-
healthyThreshold?: number // Default: 2
402+
expectedBody?: string // Optional body substring required for healthy status
403+
failureThreshold?: number // Default: 3, max 20
404+
successThreshold?: number // Default: 2, max 20
405+
minHealthyTargets?: number // Default: 1
406+
allowedSchemes?: string[] // Default: ['http', 'https']
407+
allowedHosts?: string[] // CIDR or exact hosts allowed for health checks
401408
validator?: (response: Response) => Promise<boolean> | boolean
402409
}
403410
```

docs/CLUSTERING.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,14 @@ interface ClusterConfig {
141141
// Exit master process after shutdown (default: true)
142142
// Set to false for testing or embedded usage
143143
exitOnShutdown: boolean
144+
145+
// Allow-list of environment variable names forwarded to workers.
146+
// If provided, only these variables (plus internal CLUSTER_* vars) are passed.
147+
allowedEnvVars?: string[]
148+
149+
// Allow-list of worker script paths. The worker script must match one of
150+
// these paths to prevent accidental execution of arbitrary files.
151+
workerScriptAllowlist?: string[]
144152
}
145153
```
146154

docs/LOAD_BALANCING.md

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -401,8 +401,8 @@ gateway.addRoute({
401401
timeout: 5000, // 5 second timeout
402402
path: '/health', // Health check endpoint
403403
expectedStatus: 200, // Expected status code
404-
unhealthyThreshold: 3, // Failures before marking unhealthy
405-
healthyThreshold: 2, // Successes before marking healthy
404+
failureThreshold: 3, // Failures before marking unhealthy
405+
successThreshold: 2, // Successes before marking healthy
406406
},
407407
},
408408
})
@@ -594,7 +594,7 @@ gateway.addRoute({
594594
enabled: true,
595595
interval: 10000,
596596
path: '/health',
597-
unhealthyThreshold: 2, // Fast failover
597+
failureThreshold: 2, // Fast failover
598598
},
599599
},
600600
})
@@ -650,12 +650,29 @@ healthCheck: {
650650
interval: 15000,
651651
timeout: 5000,
652652
path: '/health',
653-
unhealthyThreshold: 3,
654-
healthyThreshold: 2,
653+
failureThreshold: 3,
654+
successThreshold: 2,
655655
}
656656
```
657657

658-
### 2. Use Circuit Breakers for External Services
658+
### 2. Restrict Health Check Targets
659+
660+
For security, limit which schemes and hosts the load balancer may probe.
661+
662+
```typescript
663+
healthCheck: {
664+
enabled: true,
665+
interval: 15000,
666+
timeout: 5000,
667+
path: '/health',
668+
failureThreshold: 3,
669+
successThreshold: 2,
670+
allowedSchemes: ['http', 'https'],
671+
allowedHosts: ['10.0.0.0/8', 'api.internal.example.com'],
672+
}
673+
```
674+
675+
### 3. Use Circuit Breakers for External Services
659676

660677
```typescript
661678
circuitBreaker: {
@@ -666,7 +683,7 @@ circuitBreaker: {
666683
}
667684
```
668685

669-
### 3. Configure Timeouts
686+
### 4. Configure Timeouts
670687

671688
```typescript
672689
gateway.addRoute({
@@ -681,7 +698,7 @@ gateway.addRoute({
681698
})
682699
```
683700

684-
### 4. Monitor Target Health
701+
### 5. Monitor Target Health
685702

686703
```typescript
687704
import { PinoLogger } from 'bungate'
@@ -698,7 +715,7 @@ gateway.on('target-healthy', (target) => {
698715
})
699716
```
700717

701-
### 5. Use Appropriate Strategy
718+
### 6. Use Appropriate Strategy
702719

703720
```typescript
704721
// ❌ DON'T use IP hash for APIs behind NAT
@@ -714,7 +731,7 @@ loadBalancer: {
714731
}
715732
```
716733

717-
### 6. Plan for Capacity
734+
### 7. Plan for Capacity
718735

719736
```typescript
720737
// Configure weights based on actual capacity
@@ -728,7 +745,7 @@ loadBalancer: {
728745
}
729746
```
730747

731-
### 7. Test Failover Scenarios
748+
### 8. Test Failover Scenarios
732749

733750
```bash
734751
# Simulate server failure
@@ -781,7 +798,7 @@ targets: [
781798
healthCheck: {
782799
enabled: true,
783800
timeout: 10000, // Increase from 5000
784-
unhealthyThreshold: 5, // Require more failures
801+
failureThreshold: 5, // Require more failures
785802
}
786803

787804
// 2. Check health endpoint performance

0 commit comments

Comments
 (0)