Skip to content

Security: Abrechen2/TravStats

Security

SECURITY.md

Security — TravStats

Last audit: 2026-04-13 Scope: Full-stack (Express/TypeScript backend + React/Vite frontend), Docker deployment behind reverse proxy Approach: Black-box pentest + full-source code review — all discovered findings mitigated prior to release.


Current Status

Measure Status
Authentication (JWT HttpOnly cookie) OK
Rate limiting (15 distinct limiters) OK
Input validation (Zod on all endpoints) OK
CORS (same-origin only) OK
SQL injection protection (Prisma ORM) OK
XSS protection (React + Helmet CSP) OK
CSRF protection (SameSite=Strict cookie) OK
Security headers (Helmet + nginx) OK
Secrets management (.env, encrypted at rest) OK
Dependency vulnerabilities (npm audit) OK (0 vulnerabilities)
Shell injection protection (spawn, no exec) OK
nginx server_tokens off OK
Dotfile access blocked (nginx) OK

Security Architecture

Authentication

  • JWT stored in HttpOnly cookie (not localStorage, not Bearer header)
  • SameSite=Strict, Secure auto-detected via X-Forwarded-Proto
  • Cookie max age: 7 days
  • Bearer header fallback removed to prevent XSS extraction
  • Force-change-password token delivered via separate HttpOnly cookie

Authorization

  • All API routes behind authenticate middleware
  • Admin routes additionally behind requireAdmin middleware
  • User isolation: all queries include WHERE userId = :userId
  • Deactivated accounts cannot authenticate (checked on every request)

Rate Limiting

Endpoint Limit Key
Auth (login, register) 10 / 15 min IP
Password reset 5 / 15 min IP
Flight creation 20 / hour IP
Flight lookup (external API) 30 / 15 min IP
Batch creation 50 / hour userId or IP
Email/PDF/boarding pass parse 10-20 / 15 min userId or IP
Stats calculation 30 / min userId or IP
Admin export 5 / hour userId or IP
Backup restore 3 / hour userId or IP
Settings 60 / 15 min userId or IP
File uploads 30 / hour IP
Analytics events 100 / 15 min IP
Global API 10,000 / 15 min IP
Setup initialize 10 / 15 min IP

Input Validation

  • Zod schemas on all auth endpoints (login, register, password reset)
  • Zod schemas on flight creation, update, and query parameters
  • Typed schemas on pending-update preview and apply endpoints
  • Whitelisted event types on analytics endpoint
  • Length-bounded invitation tokens (max 128 chars)
  • Regex-validated route colors (/^#[0-9a-fA-F]{3,8}$/)
  • Scheme-restricted profile picture URLs (http/https only)
  • Body size limits: JSON max size via express.json({ limit }), payload checks in Zod

Secrets & Encryption

  • API keys (AirLabs, Aviationstack, OpenSky) encrypted at rest via encryptApiKey()
  • SMTP password encrypted at rest via encryptApiKey()
  • JWT secret auto-generated on first boot, persisted to /app/data/secrets/jwt.secret (mode 0600, inside the data volume); explicit JWT_SECRET env overrides
  • Encryption key (for AES-GCM of API keys / SMTP / WebDAV credentials) generated the same way at /app/data/secrets/encryption.key
  • No hardcoded secrets in source code; .env file gitignored
  • Docker deployment: no secrets in the compose env — the data volume alone carries the key material

Backup Security

  • Backup download: path-containment check against BACKUP_BASE_DIR
  • Backup restore: targetDatabaseUrl removed from API (server-side env only)
  • Database dump: spawn() with argument arrays (no shell injection)
  • Backup restore rate limited (3/hour)

nginx

  • server_tokens off (no version disclosure)
  • Security headers on all static responses (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy)
  • Dotfile access blocked (location ~ /\. { deny all; })
  • API headers handled exclusively by Helmet (no duplication)
  • Source map requests return 204 (no source code exposure)
  • SPA fallback via try_files (no directory listing)

Logging & Monitoring

  • Security events logged with Pino (structured JSON)
  • Failed auth attempts: IP, user-agent, URL, reason
  • Invalid tokens: logged with context
  • Admin access denials: logged with userId
  • No sensitive data in logs (passwords, tokens filtered)
  • Log files in data/logs/ (app.log, error.log, http.log)

Audit History

Date Scope Summary
2026-04-13 Black-box pentest + code review 22 findings — all CRITICAL/HIGH mitigated; remaining items accepted by design (see below)
2026-04-12 Code review + dependency audit 32 findings — all mitigated
2026-04-06 Initial black-box + code review All findings mitigated before release

Detailed per-finding reports are kept internally to avoid providing an attack roadmap for already-mitigated issues. Verification commands below let anyone reproduce the current hardened state against their own deployment.

Accepted Items (by design)

Area Reason
Temporary admin-reset password in API response Required for self-hosted instances without outgoing SMTP
Leaderboard usernames visible to signed-in users Intentional — shared tracker for small trusted groups
TLS termination Delegated to the operator's reverse proxy (nginx/Caddy/Traefik); app is LAN-only HTTP by default

Tested Attack Vectors — No Vulnerability Found

Attack Result
SQL injection (login, register, flight fields) Blocked — Prisma parameterized queries
NoSQL injection (JSON objects in string fields) Blocked — Zod type validation (400)
XSS in username/password/flight fields Blocked — Zod + React auto-escaping
JWT alg:none / signature forgery Blocked — returns 401
JWT via Bearer header Blocked — only HttpOnly cookie accepted
Cookie injection (userId, isAdmin) Blocked — only auth_token cookie used
Mass assignment (isAdmin:true in login) Blocked — Zod ignores unknown fields
CORS cross-origin (evil.com) Blocked — no Access-Control headers
Path traversal (../../etc/passwd) Blocked — nginx normalizes, 404
HTTP TRACE/TRACK Blocked — 405 Not Allowed
Open registration Blocked — requires invitation token
Force re-setup Blocked — 404 after initial setup
X-Forwarded-For rate limit bypass Not effective — proper IP extraction
Host header injection on password reset Not vulnerable — Zod validation
Oversized payload (100KB+) Blocked — body limit + rate limit
Username enumeration via login timing Protected — bcrypt always runs (dummy hash)
Password reset user enumeration Protected — generic response
Directory fuzzing (30k+ paths) No hidden endpoints or leaked files
Nuclei automated scan Zero findings
WAF detection (wafw00f) No WAF needed — LAN-only service

Verification Commands

TARGET="http://localhost:3000"   # replace with your deployment URL

# Health check (no version leak)
curl -s $TARGET/health

# Security headers on static assets
curl -sI $TARGET/assets/js/index-*.js | grep -iE "x-frame|x-content|referrer|permissions"

# Security headers on API
curl -sI $TARGET/api/v1/flights | grep -iE "x-frame|x-content|csp|hsts|referrer"

# Dotfile access blocked
curl -sI $TARGET/.env          # should be 403 Forbidden
curl -sI $TARGET/.git/config   # should be 403 Forbidden

# Auth required on all endpoints
curl -s $TARGET/api/v1/flights                # 401
curl -s $TARGET/api/v1/admin/users            # 401
curl -s $TARGET/api/v1/settings               # 401

# JWT alg:none rejected
curl -s $TARGET/api/v1/flights \
  -H 'Cookie: auth_token=eyJhbGciOiJub25lIn0.eyJ1c2VySWQiOjF9.' # 401

# Bearer header rejected
curl -s $TARGET/api/v1/flights \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjF9.fake' # 401

# CORS: no cross-origin headers for foreign origins
curl -sI $TARGET/api/v1/flights -H 'Origin: https://evil.com' | grep -i access-control
# (should return nothing)

# Rate limiting active
curl -sI $TARGET/api/v1/flights | grep -i ratelimit

# HTTP methods blocked
curl -sI -X TRACE $TARGET/    # 405
curl -sI -X OPTIONS $TARGET/  # 405

# Registration disabled
curl -s -X POST $TARGET/api/v1/auth/register \
  -H 'Content-Type: application/json' \
  -d '{"username":"test","password":"Test1234!"}' # 403

# Setup already complete
curl -s $TARGET/api/v1/setup/status # setupComplete: true

# nginx version hidden
curl -sI $TARGET/ | grep -i server  # "Server: nginx" (no version)

# npm audit clean
cd backend && npm audit
cd frontend && npm audit

# Port scan against your own deployment
nmap -sV -p- --open <your-host>
# Expected for a default deploy: HTTP port of the container only

Reporting Vulnerabilities

If you discover a security issue, please report it privately via GitHub Security Advisories. Include: affected endpoint, reproduction steps, and potential impact.

Do not open a public issue for security vulnerabilities.

There aren’t any published security advisories