corvid/SECURITY_REVIEW.md

67 lines
2.8 KiB
Markdown
Raw Permalink Normal View History

# Security Review — corvid
Generated: 2026-07-13 (automated authorized audit)
Supersedes the prior manual review dated 2026-06-02.
## Scope
All tracked files (`git ls-files`) of corvid — a FastAPI ticketing service with an
MCP server (`corvid/main.py`, `corvid/router.py`, `corvid/models.py`,
`corvid/database.py`, `mcp_server.py`), Alembic migrations, Jinja templates, and the
container/compose definitions.
## Methodology
Manual source inspection of the auth layer, all query construction, template rendering,
and config; plus greps for committed secrets and unsafe sinks. No automated scanners
were available.
## Summary
| Severity | Count |
|----------|-------|
| Critical | 0 |
| High | 0 |
| Medium | 1 |
| Low | 1 |
| Info | 1 |
## Findings
### M1 — Auth fails open when `TICKETS_API_KEY` is unset (Medium)
- **Location:** `corvid/router.py:164-165`, `corvid/config.py:4`
- **Description:** `TICKETS_API_KEY` defaults to an empty string (`config.py:4`). In
`require_ticket_auth`, when no API key is configured the handler returns a synthetic
`{"username": "local", "email": "local@localhost"}` identity instead of raising 401
(`router.py:164-165`) — i.e. the service runs fully open. The intent is a "local mode",
but it is the **default** state and there is no startup warning or refuse-to-serve guard.
- **Impact:** A deployment that forgets to set `TICKETS_API_KEY` exposes all ticket
read/write endpoints unauthenticated to anything that can reach the port. The web UI
never sends `X-Api-Key`, so it relies on this open mode unless fronted by an auth proxy.
- **Recommendation:** Fail closed — refuse to start (or 401 all requests) when both
`TICKETS_API_KEY` is empty and no `require_user` dependency is wired, unless an explicit
`CORVID_ALLOW_ANONYMOUS=1` opt-in is set.
### L1 — No rate limiting / lockout on the API-key check (Low)
- **Location:** `corvid/router.py:151-154`
- **Description:** The key comparison itself is constant-time (`hmac.compare_digest`
good), but there is no throttling on repeated failed attempts.
- **Recommendation:** Add basic rate limiting at the proxy or app layer.
### Info — Internal DB DSN via environment
`corvid/config.py`/`database.py` read the database URL from the environment; `.env` is
gitignored and `.env.example` holds only placeholders. No secrets committed.
## Clean (verified, no findings)
- **Secrets:** none committed — `.env` is gitignored; `.env.example` is placeholders only.
- **SQL injection:** all DB access is via SQLAlchemy ORM / bound parameters; no string-built SQL.
- **XSS:** Jinja templates auto-escape; no `| safe` on user-controlled data observed.
- **Command injection / unsafe deserialization:** no `subprocess`, `os.system`, `eval`,
`exec`, `pickle`, or `yaml.load` in tracked code.
- **Auth comparison:** timing-safe via `hmac.compare_digest`.