Security¶
What was checked, what was fixed, and how to re-run the checks.
Threat model (short)¶
ReckonFlow is a headless financial API. Attackers might try to:
- inject SQL through query parameters or CSV fields
- escape the upload directory with crafted filenames
- smuggle instructions through receipt text into the extractor
- corrupt money with floats
- double-post via retries
- race two reconcile workers onto the same expense
Findings and fixes¶
| Area | Severity | Status | Notes |
|---|---|---|---|
| SQL injection | High if present | OK | Queries use SQLAlchemy select/where with bound parameters. No f-string SQL found. |
| Path traversal on receipts | High | Fixed/hardened | safe_filename() strips path segments; _resolve_storage_path() ensures the final path stays under receipt_storage_dir on write and read. |
| Prompt injection via receipts | Medium | Mitigated | ReceiptExtraction uses extra="forbid"; extractor cannot approve/pay/post. See ADR 002. |
| Float money | High | OK | MoneyStr + parse_money reject float. |
| Secrets in repo | High | OK | .env gitignored; config from environment. Rotate any secret that was ever exposed (ticket, screenshot, shared terminal). |
| Auth | High | Mitigated | All /api/v1 finance routes require X-API-Key when API_KEY is set (ADR 004). Empty key disables the gate for local/CI. APP_ENV=production refuses boot without a key. |
| Metrics exposure | Low | Mitigated | GET /metrics requires the same X-API-Key when configured; open only when API_KEY is empty. |
| Duplicate POSTs | Medium | Mitigated | Idempotency middleware; fails open if Redis is down (availability over strictness — see ADR 003). |
| Concurrent match writes | Medium | Mitigated | FOR UPDATE when linking expense ↔ bank row; unique constraints on match links. |
| Rate limiting | Low | Mitigated | In-process sliding window (RATE_LIMIT_PER_MINUTE); Redis token bucket for multi-instance later. |
| Dependency CVEs | Medium | OK | CI runs pip-audit on every push; local package is skipped. |
| Debug in production | Low | OK if configured | Default DEBUG=false; set APP_ENV=production on Render. |
| CORS | Low | N/A | No browser frontend; default FastAPI CORS is closed unless you add middleware. |
How to reproduce the checks¶
# Same gates as CI
uv run ruff check src tests
uv run mypy src
uv run pytest
uv run pip-audit
uv run python scripts/run_evals.py
Manual review checklist:
- Grep for
text(f"/ string-built SQL — should be empty. - Upload a receipt named
../../tmp/evil.txt— stored name must be sanitized; path must remain under storage. - Post unbalanced ledger JSON — must fail validation or service rules.
- Replay
POSTwith the sameIdempotency-Keywhen Redis is configured.
Accepted risks¶
- Idempotency fail-open: if Redis is unreachable, retries are not shielded. Prefer fixing Redis over flipping this to fail-closed for a public demo API.
- Free-tier cold starts: not a security issue, but availability is limited.
- Shared Upstash DB: safe if key prefixes differ (
REDIS_KEY_PREFIX).