corvid/SECURITY_REVIEW.md
caoimhinr 2da7a6ec51 docs: add automated security review findings
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 05:52:50 +02:00

2.8 KiB

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.