Skip to content

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 valid Authorization: Bearer … header receive 401. 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 return 403 for 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_SECRET to 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 FixedFXQ_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) and npm audit (frontend) in CI to catch known CVEs.
  • Never commit secrets. Keep FXQ_SECRET and any database or object-store credentials in a secret manager or a git-ignored .env file.

Reporting a vulnerability

Please report suspected vulnerabilities privately to the maintainers rather than opening a public issue, so a fix can ship before disclosure.