5.3 KiB
5.3 KiB
Security Audit — Movie Night
Date: 2026-03-15 Auditor: Claude (AI-assisted review) Scope: Full codebase review (24 source files) Context: Currently exposed only on Tailscale tailnet. Planning to expose via Pangolin to the internet — these findings should be resolved before that transition.
Critical / High
1. Shared search links bypass authentication entirely
- Location:
app/routers/mood.py:187-207 - Issue:
GET /api/history/shared/{entry_id}is fully public. Sequential integer IDs make enumeration trivial. Anyone can view mood text, movie recommendations, and viewing patterns. - Fix: Add a cryptographic share token (e.g.
secrets.token_urlsafe(16)) stored alongside the history entry. Require the token as a query parameter to access the shared endpoint.
2. Prompt injection via mood text
- Location:
app/services/llm/base.py:31-48 - Issue: User-supplied
moodis interpolated directly into the LLM prompt with no sanitization or length limit. A crafted input could manipulate rankings or attempt to extract the system prompt. - Mitigating factor: The title/ID cross-validation in
app/routers/mood.py:87-94is good defense-in-depth and limits the blast radius. - Fix: (a) Add a max length on mood input (~500 chars). (b) Strip or escape prompt-like patterns. (c) Add an explicit instruction in the system prompt to ignore meta-instructions in user text.
Medium
3. Container runs as root
- Location:
Dockerfile - Issue: No
USERdirective. The app runs as root inside the container. - Fix: Add
RUN useradd -m -u 1000 appuserandUSER appuser.
4. Insecure default session secret
- Location:
app/config.py - Issue:
session_secretdefaults to"change-me-in-production". If.envis misconfigured, sessions are trivially forgeable. - Fix: Remove the default and make it a required field (pydantic will error on startup if missing).
5. No rate limiting on login
- Location:
app/routers/auth.py - Issue:
/api/auth/loginhas no rate limiting, enabling brute-force attacks against Jellyfin credentials. - Fix: Add a simple in-memory rate limiter (e.g.
slowapior a manual counter per IP with a 5-attempt/minute window).
6. No security headers
- Issue: No
Content-Security-Policy,X-Frame-Options,X-Content-Type-Options, orStrict-Transport-Securityheaders are set by the application or documented for the reverse proxy. - Fix: Add a FastAPI middleware or document the required headers for the nginx/reverse proxy config.
7. Poster proxy has no input validation or auth
- Location:
app/main.py:43-52 - Issue:
item_idis passed directly to Jellyfin with no format validation and no authentication required. This leaks library contents and is a mild SSRF vector. - Fix: Validate
item_idmatches^[a-f0-9]+$(Jellyfin ID format) and require authentication.
8. Arbitrary additional_user_ids accepted
- Location:
app/routers/mood.py:31 - Issue: The client can pass any user IDs to filter watch state. No server-side validation that the IDs belong to the household.
- Fix: Maintain an allowlist of valid Jellyfin user IDs (or fetch them server-side) instead of trusting client input.
9. Database directory permissions not set
- Location:
app/database.py:8-9 - Issue:
os.makedirsuses default umask. Other container processes could read/write the DB. - Fix:
os.makedirs(..., mode=0o700, exist_ok=True)
10. Sync endpoint has no rate limit
- Location:
app/routers/library.py:58-62 - Issue: Any authenticated user can spam
POST /api/library/sync, spawning unbounded background tasks. - Fix: Track last sync time and reject requests within a cooldown window (e.g. 10 minutes).
Low (easy wins)
| # | Finding | Location | Fix |
|---|---|---|---|
| 11 | No mood text length limit | app/routers/mood.py:25 |
Add max_length=500 to Pydantic model |
| 12 | No auth event logging | app/routers/auth.py |
Log failed/successful logins with IP + timestamp |
| 13 | No search history retention policy | app/routers/mood.py |
Add periodic cleanup (e.g. 90-day TTL) |
| 14 | Tailwind loaded from CDN | app/static/index.html:7 |
Consider Subresource Integrity (integrity= attribute) |
| 15 | No .dockerignore for .env |
.dockerignore |
Ensure .env is listed |
What's done well
- Parameterized SQL everywhere — no injection vectors found
- Secure session cookies —
HttpOnly,SameSite=Lax,Secureflag, 32-byte random IDs - LLM output validation — title/ID cross-check prevents hallucinated recommendations
- Dependencies are current — no known CVEs in pinned versions
- No secrets in source —
.envis gitignored,.env.exampleis clean
Resolution priority
Must fix before internet exposure (via Pangolin):
- Shared search link auth (#1)
- Container runs as root (#3)
- Session secret default (#4)
- Rate limiting on login (#5)
- Security headers (#6)
- Poster proxy validation (#7)
- Mood text length limit (#11)
Should fix soon after:
- Prompt injection hardening (#2)
- User ID validation (#8)
- DB permissions (#9)
- Sync rate limit (#10)
- Auth logging (#12)
Nice to have:
- History retention (#13)
- Tailwind SRI (#14)
- Dockerignore check (#15)