2026-06-02 13:27:20 +00:00
|
|
|
# Security Review — corvid
|
|
|
|
|
|
2026-07-14 05:52:50 +02:00
|
|
|
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`.
|