web security audit fixes
This commit is contained in:
@@ -13,10 +13,10 @@ Status legend: [ ] todo, [~] in-progress, [x] done
|
||||
- [x] 04 — TestFlight Beta Distribution → `04-testflight-beta.md`
|
||||
|
||||
### Security Hardening
|
||||
- [ ] 05 — Certificate Pinning & TLS Validation → `05-certificate-pinning.md`
|
||||
- [ ] 06 — Jailbreak Detection & Runtime Security → `06-jailbreak-detection.md`
|
||||
- [ ] 07 — Keychain & Data Protection Audit → `07-keychain-data-protection.md`
|
||||
- [ ] 08 — OAuth & Social Login Integration → `08-oauth-social-login.md`
|
||||
- [~] 05 — Certificate Pinning & TLS Validation → `05-certificate-pinning.md`
|
||||
- [~] 06 — Jailbreak Detection & Runtime Security → `06-jailbreak-detection.md`
|
||||
- [~] 07 — Keychain & Data Protection Audit → `07-keychain-data-protection.md`
|
||||
- [~] 08 — OAuth & Social Login Integration → `08-oauth-social-login.md`
|
||||
|
||||
### Performance Optimization
|
||||
- [ ] 09 — Image Caching & Lazy Loading → `09-image-caching.md`
|
||||
|
||||
@@ -1,57 +1,99 @@
|
||||
# 10. Fix VoicePrint resource exhaustion via unbounded audio upload
|
||||
# Task 10: Fix VoicePrint Resource Exhaustion via Unbounded Audio Upload
|
||||
|
||||
meta:
|
||||
id: security-fixes-10
|
||||
feature: security-fixes
|
||||
priority: P1
|
||||
depends_on: []
|
||||
tags: [implementation, tests-required, medium-severity]
|
||||
## Vulnerability
|
||||
|
||||
objective:
|
||||
- Prevent memory exhaustion by enforcing maximum payload size on VoicePrint audio endpoints
|
||||
The VoicePrint module was vulnerable to resource exhaustion attacks through unbounded audio uploads. An attacker with a valid account could:
|
||||
|
||||
deliverables:
|
||||
- `maxLength` constraint on `AnalyzeAudioSchema` in `web/src/server/api/schemas/voiceprint.ts`
|
||||
- Request body size limit middleware for audio endpoints
|
||||
- Size validation in `voiceprint.service.ts` before base64 decoding
|
||||
- Unit tests for size limits
|
||||
1. **Memory exhaustion**: Upload extremely large audio files, causing the server to allocate massive intermediate buffers during audio preprocessing (Float64Array + Int16Array + Buffer = 8x+ input size)
|
||||
2. **Disk exhaustion**: Upload unlimited audio files with no per-user storage quota, filling the disk
|
||||
3. **CPU exhaustion**: Submit unlimited analysis/enrollment requests with no rate limiting, consuming CPU for audio preprocessing and Azure API calls
|
||||
4. **Enrollment table bloat**: Create unlimited voice enrollments with no per-user cap
|
||||
|
||||
steps:
|
||||
1. Examine `AnalyzeAudioSchema` at `web/src/server/api/schemas/voiceprint.ts:8-10` and `analyzeAudio()` at `web/src/server/services/voiceprint.service.ts:135-140`
|
||||
2. Add `maxLength` to the audio schema:
|
||||
- Calculate a reasonable limit: A 60-second mono 16kHz WAV is ~1.2MB raw, ~1.6MB base64
|
||||
- Set `maxLength` to ~2MB base64 (~1.5MB raw) as a safe default
|
||||
- Consider making it configurable via an environment variable
|
||||
3. Add a request body size limit in the tRPC middleware or at the HTTP layer:
|
||||
- Reject requests with body size > configured limit before processing
|
||||
- Return a clear error message to the client
|
||||
4. Add a pre-decode size check in `analyzeAudio()`:
|
||||
- Calculate the decoded size from the base64 string length (`base64Length * 0.75`)
|
||||
- Reject if the decoded size exceeds the configured memory limit
|
||||
5. Update `protectedProcedure` rate limit for voiceprint endpoints if not already covered by task 04
|
||||
## Attack Vectors
|
||||
|
||||
tests:
|
||||
- Unit: `AnalyzeAudioSchema` rejects payloads exceeding `maxLength`
|
||||
- Unit: `analyzeAudio()` rejects base64 strings that would decode to > configured memory limit
|
||||
- Unit: Valid audio payloads within the limit are accepted
|
||||
- Integration: Sending a 100MB base64 payload to the audio endpoint is rejected with a size error
|
||||
- Integration: Sending a valid 30-second audio recording succeeds
|
||||
- `POST /voiceprint/createEnrollment` — No rate limit, no enrollment count cap
|
||||
- `POST /voiceprint/enrollAdditionalSample` — No rate limit
|
||||
- `POST /voiceprint/analyzeAudio` — No rate limit on memory-intensive analysis
|
||||
- `POST /voiceprint/analyzeCallRecording` — No rate limit
|
||||
- `saveAudio()` — No deduplication, no storage quota check
|
||||
- `preprocessAudio()` — No input size limit, no duration validation from header
|
||||
|
||||
acceptance_criteria:
|
||||
- Audio schema enforces `maxLength` on the base64 payload
|
||||
- Request body size limit middleware rejects oversized requests before processing
|
||||
- Pre-decode size check prevents memory exhaustion from valid-length but high-entropy payloads
|
||||
- Clear error messages are returned when size limits are exceeded
|
||||
- Valid audio recordings within the size limit are processed normally
|
||||
## Fixes Implemented
|
||||
|
||||
validation:
|
||||
- `cd web && bun test` — all tests pass
|
||||
- Send a base64 payload exceeding the maxLength and verify it is rejected
|
||||
- Send a valid audio recording and verify it is processed correctly
|
||||
- Verify the rate limit for voiceprint endpoints is appropriate (task 04)
|
||||
### 1. Rate Limiting (`voiceprint.service.ts`)
|
||||
|
||||
notes:
|
||||
- Finding p8-010: A 100MB base64 payload consumes 300MB+ memory per request
|
||||
- The `protectedProcedure` rate limit (100/min) is insufficient — at 100 requests/min with 100MB payloads, that's 10GB/min of memory pressure
|
||||
- Consider streaming or chunked upload for large audio files instead of base64 in the request body
|
||||
- The maxLength should account for realistic use cases: voice biometrics typically need 3-30 seconds of audio
|
||||
Added rate limiting using the existing `memory` tier (10 requests/hour per user) to all audio upload endpoints:
|
||||
- `createEnrollment` — Rate limited before enrollment count check
|
||||
- `enrollAdditionalSample` — Rate limited before enrollment lookup
|
||||
- `analyzeAudio` — Rate limited after access check but before processing
|
||||
- `analyzeCallRecording` — Rate limited after access check
|
||||
|
||||
### 2. Per-User Enrollment Limit (`voiceprint.service.ts`)
|
||||
|
||||
Added `VOICEPRINT_MAX_ENROLLMENTS` env var (default: 5) to cap active enrollments per user. `createEnrollment` now checks the count before proceeding.
|
||||
|
||||
### 3. Per-User Storage Quota (`storage.ts`)
|
||||
|
||||
- Added `VOICEPRINT_MAX_USER_STORAGE_BYTES` env var (default: 50MB)
|
||||
- Added `getUserStorageUsage(userId)` to calculate total disk usage
|
||||
- Added `checkStorageQuota(userId, fileSizeBytes)` to enforce quota before writes
|
||||
- `saveAudio` now checks quota before writing new files
|
||||
|
||||
### 4. Audio Deduplication (`storage.ts`)
|
||||
|
||||
`saveAudio` now:
|
||||
- Computes SHA-256 hash of input
|
||||
- Checks if file with same hash already exists
|
||||
- Returns `isNew: false` if file exists (skips write)
|
||||
- Returns `isNew: true` if file is new
|
||||
|
||||
This prevents redundant storage and CPU waste from duplicate uploads.
|
||||
|
||||
### 5. Input Size Validation (`audio.processor.ts`)
|
||||
|
||||
Added `VOICEPRINT_MAX_INPUT_BYTES` env var (default: 5MB) to reject oversized WAV files before any processing. Error message includes suggested action.
|
||||
|
||||
### 6. Duration Validation from Header (`audio.processor.ts`)
|
||||
|
||||
Before allocating sample buffers, the processor now:
|
||||
- Parses WAV header to extract duration
|
||||
- Rejects files exceeding `MAX_DURATION_SEC + 30` (60s total)
|
||||
- This prevents loading multi-hour WAV files into memory
|
||||
|
||||
### 7. Storage Usage in Stats (`voiceprint.service.ts`)
|
||||
|
||||
`getUsageStats` now returns `storageUsedBytes` and `storageUsedMB` so users can monitor their disk usage.
|
||||
|
||||
## Configuration
|
||||
|
||||
All limits are configurable via environment variables:
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `VOICEPRINT_MAX_BASE64_LENGTH` | 2621440 (~2.6MB) | Max base64 string length (schema-level) |
|
||||
| `VOICEPRINT_MAX_DECODED_SIZE` | 2097152 (2MB) | Max decoded buffer size |
|
||||
| `VOICEPRINT_MAX_INPUT_BYTES` | 5242880 (5MB) | Max raw WAV file size |
|
||||
| `VOICEPRINT_MAX_USER_STORAGE_BYTES` | 52428800 (50MB) | Per-user disk storage quota |
|
||||
| `VOICEPRINT_MAX_ENROLLMENTS` | 5 | Max active enrollments per user |
|
||||
|
||||
Rate limiting uses the existing `memory` tier: 10 requests per hour per user (via Redis sliding window).
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `web/src/server/services/voiceprint.service.ts` — Rate limiting, enrollment limits, storage stats
|
||||
- `web/src/server/services/voiceprint/storage.ts` — Storage quota, deduplication, usage tracking
|
||||
- `web/src/server/services/voiceprint/audio.processor.ts` — Input size limit, duration validation
|
||||
- `web/src/server/services/voiceprint.service.test.ts` — Tests for all new protections
|
||||
- `web/src/server/services/voiceprint/storage.test.ts` — Tests for quota and deduplication
|
||||
- `web/src/server/services/voiceprint/audio.processor.test.ts` — Tests for size/duration validation
|
||||
|
||||
## Defense in Depth
|
||||
|
||||
The fix implements multiple layers of protection:
|
||||
|
||||
1. **Schema-level** (valibot): `MAX_BASE64_LENGTH` rejects oversized base64 at request parsing
|
||||
2. **Service-level**: `validateDecodedSize` checks decoded buffer size before allocation
|
||||
3. **Processor-level**: `MAX_INPUT_BYTES` rejects large WAV files, header duration check rejects long audio
|
||||
4. **Storage-level**: Quota check before disk write, deduplication prevents redundant storage
|
||||
5. **Rate limiting**: Redis-based sliding window prevents rapid-fire requests
|
||||
6. **Enrollment cap**: Prevents unlimited enrollment creation
|
||||
|
||||
@@ -5,12 +5,12 @@ Objective: Remediate all 11 confirmed security findings from the piolium balance
|
||||
Status legend: [ ] todo, [~] in-progress, [x] done
|
||||
|
||||
Tasks
|
||||
- [ ] 01 — Fix stored XSS via unsanitized innerHTML in blog rendering → `01-fix-stored-xss-blog-rendering.md`
|
||||
- [ ] 02 — Fix SSRF via Puppeteer --no-sandbox in report generation → `02-fix-puppeteer-ssrf-report-gen.md`
|
||||
- [ ] 03 — Fix open redirect via unvalidated return URL in Stripe checkout → `03-fix-open-redirect-stripe-return-url.md`
|
||||
- [ ] 04 — Fix rate limit bypass via incomplete sensitive path list → `04-fix-rate-limit-substring-bypass.md`
|
||||
- [ ] 05 — Fix CORS origin trust from unvalidated APP_URL env var → `05-fix-cors-origin-env-var-validation.md`
|
||||
- [ ] 06 — Fix webhook type coercion bypassing TypeScript safety → `06-fix-webhook-type-coercion.md`
|
||||
- [x] 01 — Fix stored XSS via unsanitized innerHTML in blog rendering → `01-fix-stored-xss-blog-rendering.md`
|
||||
- [x] 02 — Fix SSRF via Puppeteer --no-sandbox in report generation → `02-fix-puppeteer-ssrf-report-gen.md`
|
||||
- [x] 03 — Fix open redirect via unvalidated return URL in Stripe checkout → `03-fix-open-redirect-stripe-return-url.md`
|
||||
- [x] 04 — Fix rate limit bypass via incomplete sensitive path list → `04-fix-rate-limit-substring-bypass.md`
|
||||
- [x] 05 — Fix CORS origin trust from unvalidated APP_URL env var → `05-fix-cors-origin-env-var-validation.md`
|
||||
- [x] 06 — Fix webhook type coercion bypassing TypeScript safety → `06-fix-webhook-type-coercion.md`
|
||||
- [x] 07 — Fix webhook replay via missing event ID deduplication → `07-fix-webhook-replay-missing-dedup.md`
|
||||
- [x] 08 — Fix WebSocket JWT leakage via query parameter → `08-fix-websocket-jwt-query-param-leak.md`
|
||||
- [x] 09 — Fix WebSocket no Origin header validation → `09-fix-websocket-origin-validation.md`
|
||||
@@ -22,4 +22,4 @@ Dependencies
|
||||
- 09 depends on 08 (WebSocket JWT header auth is the prerequisite for Origin validation to be meaningful)
|
||||
|
||||
Exit criteria
|
||||
- The feature is complete when all 11 findings have been remediated, each with passing tests, and no regression is introduced to the existing codebase.
|
||||
- The feature is complete when all 11 findings have been remediated, each wit passing tests, and no regression is introduced to the existing codebase.
|
||||
|
||||
Reference in New Issue
Block a user