Security model & review¶
This page documents fxquinox's security posture and the findings (with fixes) from the pre-1.0 security review. For day-to-day setup, follow the production hardening checklist.
Trust model¶
- Authentication required by default. Every API endpoint requires a valid
principal (a JWT bearer token or an
fxq_-prefixed API key). Requests without a validAuthorization: Bearer …header receive401. A short, deliberate allowlist is reachable without a token: POST /auth/login,POST /auth/register— bootstrap a session.GET /healthz— liveness probe.GET /media/{key}— capability-style unguessable URLs served directly to<img>tags that cannot carry an Authorization header.GET /shared/{token},POST /shared/{token}/notes— the client-review share link (scoped to one playlist by an unguessable, revocable token; a guest can read the review and post a scoped note + approval, nothing else).
Enforcement is centralised in fxq.api.deps and covered by a test
(test_authz.py) that walks the full route table — a new endpoint added
without auth fails the build. See the
authentication reference for
the full rationale.
-
Authorization enforced server-side. Schema and pipeline configuration is
Admin-only (mutating schema endpoints return403for any other role). Cross-user timesheet access is limited to the privileged roles: Admin, Supervisor, Coordinator. The UI hides these actions for lower roles, but the API is the enforcement boundary — never the UI. -
Passwords are bcrypt-hashed. API keys are stored as SHA-256 hashes plus a short display prefix; the raw
fxq_…token is shown exactly once on creation and never persisted in plaintext. -
Transport. Terminate TLS at a reverse proxy in front of the API. The app itself serves plain HTTP; do not expose it directly to the internet.
-
JWT tokens use HS256 with a 1-hour expiry. Rotate
FXQ_SECRETto invalidate all live sessions.
Review findings and fixes¶
The 1.0 security pass covered JWT handling, API-key storage, authorization completeness, file-upload safety, and the query-DSL injection surface.
| # | Finding | Severity | Status |
|---|---|---|---|
| 1 | Reads (and several writes) were unauthenticated | High | Fixed — auth enforced on all endpoints; documented public allowlist; route-table test added. |
| 2 | GET /media/{key} could resolve outside the media root (path traversal) |
High | Fixed — keys are resolved against the root and rejected if they escape it (LocalStorage._safe). |
| 3 | Production could boot with a built-in default JWT secret | High | Fixed — FXQ_SECRET is required (≥ 32 chars) on any non-SQLite backend; the app refuses to start otherwise. |
| 4 | API keys stored in plaintext in the record store | Medium | Fixed — only a SHA-256 hash and display prefix are stored. |
| 5 | Uploads had no size limit (memory-exhaustion risk) | Medium | Fixed — 25 MB cap on attachments/thumbnails, 1 GB on reviewables (413 otherwise). |
| 6 | contains/starts_with/ends_with filters did not escape LIKE wildcards |
Low | Fixed — % and _ are escaped so filters match literally. |
| 7 | JWTs use HS256 with a 1-hour expiry; no rotation mechanism | Info | Accepted for 1.0. Rotate FXQ_SECRET to invalidate all tokens. |
| 8 | Open registration (POST /auth/register is public) |
Info | By design for bootstrap. Gate it at the reverse proxy for locked-down installs. |
No SQL injection in the query DSL
The filter DSL validates operators against a whitelist and compiles to SQLAlchemy expressions — both values and JSON keys are bound as parameters, never string-interpolated into SQL.
Webhooks and outbound requests¶
Webhooks (Settings → Automation → Webhooks) fire signed HTTPS POST requests
when records change. Payloads use an HMAC-SHA256 signature over the request
body, delivered as X-FXQ-Signature. Verify the signature on the receiving end
before acting on the payload. The delivery log (last N attempts and their HTTP
status) is visible in the Webhooks settings panel.
Dependency and CI hygiene¶
- Dependabot is enabled for dependency updates.
- Run
pip-audit(Python) andnpm audit(frontend) in CI to catch known CVEs. - Never commit secrets. Keep
FXQ_SECRETand any database or object-store credentials in a secret manager or a git-ignored.envfile.
Reporting a vulnerability¶
Please report suspected vulnerabilities privately to the maintainers rather than opening a public issue, so a fix can ship before disclosure.