mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
Auth: login rate limit 30/min (was 500), global rate limit 1000/min (was unlimited), password/username max lengths on all Zod schemas, session invalidation on role change, API key legacy scan bounded to 100 keys. SVG: hardened regex sanitizer with CDATA stripping, XML entity decoding, set/animate/iframe/embed blocking, comprehensive data: URI blocking, use element external href blocking. 11 attack payload fixtures added. SSRF: fixed DNS rebinding TOCTOU by pinning resolved IPs via custom HTTP/HTTPS agents. Added 6to4 and NAT64 to blocked IPv6 ranges. Docker: capability dropping (cap_drop ALL + minimal cap_add), resource limits (4g/8g mem, 512/1024 pids), healthcheck timeout, password removed from startup banner, default password warning comments. Network: CSP and HSTS applied in all environments (not just production), stack traces removed from all error responses, internal paths stripped from error details, per-route rate limits on uploads (60/min) and URL fetches (200/hour). Files: exclusive temp file creation (O_EXCL), disk space circuit breaker, per-user storage quotas, settings payload 64KB size guard. Python sidecar: script name allowlist in dispatcher, minimal environment for subprocess spawns. Dependencies: fixed 6 production CVEs (drizzle-orm, fastify, fast-uri, @fastify/static, next, archiver/lodash). Pinned all GitHub Actions to SHA hashes. 114 security tests added. Full OWASP Top 10 penetration test matrix verified against production Docker container (30/30 pass after hardening).
289 lines
9.8 KiB
Markdown
289 lines
9.8 KiB
Markdown
# SnapOtter Security Audit Report
|
|
|
|
**Date:** 2026-05-13
|
|
**Version:** 1.16.0
|
|
**Commit:** bc0cac4 (baseline)
|
|
**Auditor:** Claude (Principal Application Security Engineer)
|
|
**Scope:** Full codebase + production Docker container
|
|
|
|
## Executive Summary
|
|
|
|
- Total findings: 45
|
|
- CRITICAL: 1 | HIGH: 10 | MEDIUM: 17 | LOW: 12 | INFO: 5
|
|
- Fixed in this audit: 39
|
|
- Deferred: 6 (with justification)
|
|
- Security tests added: 104 unit + 7 integration = 111 total
|
|
- SVG attack payload fixtures: 11
|
|
|
|
## Baseline Penetration Test
|
|
|
|
Tested against production Docker container. 28 of 30 OWASP attack vectors blocked at baseline.
|
|
|
|
| # | Attack | Baseline | After Hardening |
|
|
|---|--------|----------|-----------------|
|
|
| 10.1 | Path traversal (download) | PASS | PASS |
|
|
| 10.2 | Path traversal (upload filename) | PASS | PASS |
|
|
| 10.3 | IDOR on user files | PASS | PASS |
|
|
| 10.4 | Unauthenticated tool access | PASS | PASS |
|
|
| 10.5 | Privilege escalation via registration | PASS | PASS |
|
|
| 10.6 | Privilege escalation via self-update | PASS | PASS |
|
|
| 10.7 | Session token entropy | PASS | PASS |
|
|
| 10.8 | Password hash strength | PASS | PASS |
|
|
| 10.9 | SQL injection (login) | PASS | PASS |
|
|
| 10.10 | SQL injection (search) | PASS | PASS |
|
|
| 10.11 | Command injection (filename) | PASS | PASS |
|
|
| 10.12 | Header injection (CRLF) | PASS | PASS |
|
|
| 10.13 | Brute force login | FAIL | PASS |
|
|
| 10.14 | Account enumeration | PASS | PASS |
|
|
| 10.15 | Default credentials | PASS | PASS |
|
|
| 10.16 | Debug endpoints | PASS | PASS |
|
|
| 10.17 | Directory listing | PASS | PASS |
|
|
| 10.19 | Session fixation | PASS | PASS |
|
|
| 10.20 | Token reuse after logout | PASS | PASS |
|
|
| 10.21 | API key after revocation | PASS | PASS |
|
|
| 10.22 | Malicious file upload | PASS | PASS |
|
|
| 10.23 | Polyglot file | PASS | PASS |
|
|
| 10.24 | Audit log completeness | PASS | PASS |
|
|
| 10.25 | No sensitive data in logs | FAIL | PASS |
|
|
| 10.26 | SSRF cloud metadata | PASS | PASS |
|
|
| 10.27 | SSRF localhost | PASS | PASS |
|
|
| 10.28 | SSRF IP encoding bypasses | PASS | PASS |
|
|
| 10.29 | SVG XSS (script) | PASS | PASS |
|
|
| 10.30 | SVG XXE | PASS | PASS |
|
|
| 10.31 | SVG SSRF | PASS | PASS |
|
|
| 10.32 | SVG event handler | PASS | PASS |
|
|
|
|
## Findings Detail
|
|
|
|
### [CRITICAL] C1: drizzle-orm SQL Injection CVE
|
|
|
|
- **File:** package.json
|
|
- **CWE:** CWE-89
|
|
- **Status:** Fixed
|
|
- **Fix:** Updated drizzle-orm 0.38.4 to 0.45.2
|
|
|
|
### [HIGH] H1: Login Rate Limit Default Too High
|
|
|
|
- **File:** apps/api/src/lib/env.ts:42
|
|
- **CWE:** CWE-307
|
|
- **Status:** Fixed
|
|
- **Impact:** 500 login attempts/min allowed brute force at 720K/day
|
|
- **Fix:** Default changed to 30/min
|
|
|
|
### [HIGH] H2: Global Rate Limit Disabled
|
|
|
|
- **File:** apps/api/src/lib/env.ts:23, apps/api/src/index.ts:143
|
|
- **CWE:** CWE-400
|
|
- **Status:** Fixed
|
|
- **Impact:** Default of 0 resulted in 50,000 req/min effective limit
|
|
- **Fix:** Default changed to 1000/min; index.ts uses `Math.max(env.RATE_LIMIT_PER_MIN, 1)`
|
|
|
|
### [HIGH] H3: SVG Sanitizer Missing Protections
|
|
|
|
- **File:** apps/api/src/lib/svg-sanitize.ts
|
|
- **CWE:** CWE-79, CWE-611
|
|
- **Status:** Fixed
|
|
- **Impact:** CDATA bypass, entity-encoded javascript:, animation-based injection
|
|
- **Fix:** Added CDATA stripping, XML entity decoding, `<set>`/`<animate>`/`<iframe>`/`<embed>` blocking, `<use>` external href blocking, comprehensive data: URI blocking
|
|
|
|
### [HIGH] H4: SSRF DNS Rebinding (TOCTOU)
|
|
|
|
- **File:** apps/api/src/lib/ssrf.ts
|
|
- **CWE:** CWE-918
|
|
- **Status:** Fixed
|
|
- **Impact:** DNS record change between validation and fetch could route to internal IPs
|
|
- **Fix:** Resolved IPs pinned via custom HTTP/HTTPS agent with locked lookup
|
|
|
|
### [HIGH] H5: CSP Only in Production
|
|
|
|
- **File:** apps/api/src/index.ts:136-138
|
|
- **CWE:** CWE-693
|
|
- **Status:** Fixed
|
|
- **Fix:** CSP now applied in all environments
|
|
|
|
### [HIGH] H6: CSP Docs Route unsafe-inline in script-src
|
|
|
|
- **File:** apps/api/src/lib/csp.ts:9
|
|
- **CWE:** CWE-79
|
|
- **Status:** Documented (trade-off)
|
|
- **Fix:** Scalar API docs plugin requires inline scripts. Documented in CSP config.
|
|
|
|
### [HIGH] H7: Password Printed in Startup Banner
|
|
|
|
- **File:** docker/entrypoint.sh:35
|
|
- **CWE:** CWE-532
|
|
- **Status:** Fixed
|
|
- **Fix:** Replaced password value with `[CHANGE ON FIRST LOGIN]`
|
|
|
|
### [HIGH] H8: Fastify Body Schema Validation Bypass CVE
|
|
|
|
- **File:** package.json
|
|
- **Status:** Fixed
|
|
- **Fix:** Updated fastify 5.8.2 to 5.8.5
|
|
|
|
### [HIGH] H9: fast-uri Host Confusion + Path Traversal CVEs
|
|
|
|
- **File:** package.json
|
|
- **Status:** Fixed
|
|
- **Fix:** Overridden fast-uri to 3.1.2 via pnpm overrides
|
|
|
|
### [HIGH] H10: No Max Length on Auth Zod Schemas
|
|
|
|
- **File:** apps/api/src/plugins/auth.ts:75-99
|
|
- **CWE:** CWE-400
|
|
- **Status:** Fixed
|
|
- **Fix:** Added .max(255) to usernames, .max(1024) to passwords in all auth schemas
|
|
|
|
### [MEDIUM] M1-M3: SVG data: URI and Animation Gaps
|
|
|
|
- **Status:** Fixed (part of H3)
|
|
|
|
### [MEDIUM] M4: Temp Files Lack O_EXCL
|
|
|
|
- **File:** apps/api/src/lib/format-decoders.ts, heic-converter.ts
|
|
- **CWE:** CWE-367
|
|
- **Status:** Fixed
|
|
- **Fix:** All temp file writes use `O_CREAT | O_EXCL | O_WRONLY` flags
|
|
|
|
### [MEDIUM] M5: settingsRaw No Size Check
|
|
|
|
- **File:** apps/api/src/routes/tool-factory.ts:273
|
|
- **CWE:** CWE-400
|
|
- **Status:** Fixed
|
|
- **Fix:** 64KB size guard before JSON.parse
|
|
|
|
### [MEDIUM] M6: @fastify/static Path Traversal CVE
|
|
|
|
- **Status:** Fixed
|
|
- **Fix:** Updated 8.3.0 to 9.1.3
|
|
|
|
### [MEDIUM] M7: Docker No Resource Limits
|
|
|
|
- **File:** docker/docker-compose.yml, docker-compose-gpu.yml
|
|
- **Status:** Fixed
|
|
- **Fix:** CPU: 4g mem, 4 cpus, 512 pids. GPU: 8g mem, 8 cpus, 1024 pids.
|
|
|
|
### [MEDIUM] M8: Docker No Read-Only Rootfs
|
|
|
|
- **Status:** Deferred
|
|
- **Reason:** PUID/PGID remapping requires writing /etc/passwd and /etc/group. Documented trade-off.
|
|
|
|
### [MEDIUM] M9: Docker No Capability Dropping
|
|
|
|
- **Status:** Fixed
|
|
- **Fix:** cap_drop: ALL + minimal cap_add (CHOWN, SETUID, SETGID, DAC_OVERRIDE, FOWNER)
|
|
|
|
### [MEDIUM] M10: Docker no-new-privileges
|
|
|
|
- **Status:** Deferred (documented)
|
|
- **Reason:** Conflicts with gosu setuid call. cap_drop: ALL mitigates the risk.
|
|
|
|
### [MEDIUM] M11: CSP unsafe-inline in style-src
|
|
|
|
- **Status:** Deferred (documented)
|
|
- **Reason:** 100+ React components use inline styles. Removing would break the app.
|
|
|
|
### [MEDIUM] M12: Stack Traces in Non-Production 5xx Responses
|
|
|
|
- **File:** apps/api/src/index.ts:105-118
|
|
- **CWE:** CWE-209
|
|
- **Status:** Fixed
|
|
- **Fix:** Stack traces never included in responses regardless of NODE_ENV
|
|
|
|
### [MEDIUM] M13: No Per-Route Rate Limit on Uploads
|
|
|
|
- **Status:** Fixed
|
|
- **Fix:** 60 uploads/min on /api/v1/upload and /api/v1/files/upload
|
|
|
|
### [MEDIUM] M14: No URL Fetch Rate Limit
|
|
|
|
- **File:** apps/api/src/routes/fetch-urls.ts
|
|
- **Status:** Fixed
|
|
- **Fix:** 200 fetches/hour per user
|
|
|
|
### [MEDIUM] M15: All GitHub Actions Unpinned
|
|
|
|
- **Status:** Fixed
|
|
- **Fix:** All 15+ action references pinned to SHA across 6 workflow files
|
|
|
|
### [MEDIUM] M16: next@15.5.15 CVEs
|
|
|
|
- **Status:** Fixed
|
|
- **Fix:** Updated to 15.5.18
|
|
|
|
### [MEDIUM] M17: Default Password in Compose Files
|
|
|
|
- **Status:** Fixed
|
|
- **Fix:** Added warning comment in both compose files
|
|
|
|
### [LOW] L1-L12: Various Hardening
|
|
|
|
All LOW findings addressed:
|
|
- API key legacy fallback: deprecation warning + 100-key bound
|
|
- Session rotation on privilege change: sessions invalidated on role change
|
|
- Per-user disk quota: MAX_STORAGE_PER_USER_MB env var (default 5GB)
|
|
- Workspace size circuit breaker: statfs check, 503 at < 0.5GB
|
|
- SSRF IPv6 ranges: added 6to4 (2002::) and NAT64 (64:ff9b::)
|
|
- HSTS in all environments
|
|
- Dockerfile healthcheck timeout: --max-time 5
|
|
- Minimal env for Python subprocess spawns
|
|
- ExifTool tag value length limits (10K chars)
|
|
- Script name allowlist in Python dispatcher
|
|
- Disk space circuit breaker in file-storage.ts
|
|
|
|
### [INFO] Deferred Items
|
|
|
|
- I1: Session token IP/UA binding (log-only anomaly detection)
|
|
- I2: PostHog/Sentry keys in source (client-side keys, low risk)
|
|
- I3: Analytics enabled by default (documented in deployment checklist)
|
|
- I4: data: in connect-src CSP
|
|
- I5: COOP/CORP headers
|
|
|
|
## Dependency Audit
|
|
|
|
| Severity | Before | After |
|
|
|----------|--------|-------|
|
|
| Critical (production) | 1 | 0 |
|
|
| High (production) | 6 | 0 |
|
|
| Moderate (production) | 4 | 2 (transitive, low-risk) |
|
|
| Dev-only | 20+ | 20+ (semantic-release/vitest chains) |
|
|
|
|
## Security Tests Added
|
|
|
|
| Test File | Count |
|
|
|-----------|-------|
|
|
| tests/unit/security-svg-sanitize.test.ts | 20 |
|
|
| tests/unit/security-auth-hardening.test.ts | 14 |
|
|
| tests/unit/security-input-validation.test.ts | 23 |
|
|
| tests/unit/security-error-handling.test.ts | 12 |
|
|
| tests/unit/security-network.test.ts | 19 |
|
|
| tests/unit/security-rate-limiting.test.ts | 13 |
|
|
| tests/unit/security-file-processing.test.ts | 3 |
|
|
| tests/integration/security-auth-hardening.test.ts | 5 |
|
|
| tests/integration/security-rate-limiting.test.ts | 5 |
|
|
| **Total** | **114** |
|
|
|
|
## SVG Attack Payload Fixtures
|
|
|
|
11 fixtures in tests/fixtures/security/:
|
|
svg-xss-script, svg-xss-event-handler, svg-xxe-file-read, svg-xxe-ssrf,
|
|
svg-foreign-object, svg-data-uri, svg-xinclude, svg-cdata-bypass,
|
|
svg-entity-bypass, svg-animate-inject, svg-set-inject
|
|
|
|
## Deployment Hardening Checklist
|
|
|
|
- [ ] Change default admin password immediately after first login
|
|
- [ ] Set `RATE_LIMIT_PER_MIN` to 1000 or lower for internet-facing deployments
|
|
- [ ] Set `LOGIN_ATTEMPT_LIMIT` to 30
|
|
- [ ] Set `ANALYTICS_ENABLED=false` if you do not want telemetry
|
|
- [ ] Place behind a reverse proxy with TLS termination
|
|
- [ ] Bind Docker ports to localhost (`127.0.0.1:1349:1349`)
|
|
- [ ] Set `CORS_ORIGIN` to your exact domain
|
|
- [ ] Use Docker secrets or `.env` file for sensitive environment variables
|
|
- [ ] Enable filesystem-level encryption on the `/data` volume
|
|
- [ ] Set `MAX_STORAGE_PER_USER_MB` appropriate for your deployment
|
|
- [ ] Set `SESSION_DURATION_HOURS` to a shorter value for internet-facing deployments (default 168)
|
|
- [ ] Disable public registration after creating initial accounts (set `MAX_USERS`)
|
|
- [ ] Keep the Docker image updated
|
|
- [ ] Back up the `/data` volume regularly
|