- mcp_server.py: use shlex.quote() on all shell-interpolated values to prevent command injection via ticket title/description/human_id - corvid/router.py: add alphanumeric-only regex validation for environment prefix; add full ancestor cycle detection before setting parent_id; import re - Add SECURITY_REVIEW.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
46 lines
2.1 KiB
Markdown
46 lines
2.1 KiB
Markdown
# Security Review — corvid
|
||
|
||
_Reviewed: 2026-06-02_
|
||
|
||
## Critical
|
||
|
||
| ID | File | Line(s) | Issue |
|
||
|----|------|---------|-------|
|
||
| C1 | `mcp_server.py` | 49–71 | Command injection via unescaped `title`/`description`/`human_id` in shell string — use `shlex.quote()` |
|
||
| C2 | `corvid/router.py` | 162–163 | Full auth bypass when `TICKETS_API_KEY` is empty (the default) |
|
||
|
||
## High
|
||
|
||
| ID | File | Line(s) | Issue |
|
||
|----|------|---------|-------|
|
||
| H1 | `corvid/router.py` | 149–164 | No rate limiting — API key is brute-forceable |
|
||
| H2 | `corvid/main.py`, `corvid/router.py` | — | No CSRF protection (latent for cookie-auth integration) |
|
||
| H3 | `.env.example` | 3 | Weak default DB password (`changeme`) |
|
||
| H4 | `corvid/templates/tickets_base.html` | 795 | Stored XSS via unescaped `human_id` in child-ticket list |
|
||
|
||
## Medium
|
||
|
||
| ID | File | Line(s) | Issue |
|
||
|----|------|---------|-------|
|
||
| M1 | `mcp_server.py` | 75 | Synchronous HTTP client in async context (DoS risk) |
|
||
| M2 | `corvid/router.py` | 30–58 | No input length limits; no pagination |
|
||
| M3 | `corvid/main.py` | — | No Content-Security-Policy or security headers |
|
||
| M4 | `corvid/router.py` | 349–358 | Hard-delete without soft-delete or server-side guard |
|
||
| M5 | `corvid/router.py` | 331–334 | Incomplete cycle detection for ticket parent chains (only checks direct self-reference) |
|
||
|
||
## Low
|
||
|
||
| ID | File | Line(s) | Issue |
|
||
|----|------|---------|-------|
|
||
| L1 | `docker-compose.yml` | 4–15 | `POSTGRES_PASSWORD` and `DATABASE_URL` are independent, can drift |
|
||
| L2 | `alembic/env.py` | 26 | Default fallback DB URL could point to production unintentionally |
|
||
| L3 | `corvid/main.py` | 41–43 | Unauthenticated `/health` endpoint |
|
||
| L4 | `CLAUDE.md` | 16 | Hardcoded home directory path in documentation |
|
||
|
||
## Fixed in this review
|
||
|
||
- C1: `shlex.quote()` applied to all interpolated values in `mcp_server.py`
|
||
- C2: Startup check — refuse to start if `TICKETS_API_KEY` is empty
|
||
- H4: `escHtml()` applied to `child.human_id` in `tickets_base.html`
|
||
- H4: `prefix` validated against alphanumeric-only regex in `router.py`
|
||
- M5: Full cycle detection for parent chains
|