Skip to content

Security: Guembri01/drawmark-ai

Security

docs/SECURITY.md

Security

What's implemented today and what's still on the roadmap. The implemented bits are verifiable in backend/app/main.py, backend/app/auth.py, and backend/app/config.py.


Auth model

  • Password storagepasslib[bcrypt] (bcrypt work factor at the passlib default of 12). Plaintext passwords never touch the DB or the logs.
  • LoginPOST /api/v1/auth/login accepts {email, password}, returns {access_token, token_type, user}. Email is the canonical identifier and is uniquely indexed on users.
  • Access token — JWT signed with HS256, exp = now + access_token_expire_minutes (default 30). Claim payload is minimal: sub (user id), email, exp, planned org_id + token_type.
  • Authorization — every router dependency that calls Depends(get_current_user) validates the token, loads the user, and rejects inactive users. Routes that don't list the dep are open (currently only /health, /auth/login, /auth/register, /api/docs, /openapi.json).
  • Logout — client-side (drop the token). No server revocation list today; tokens are valid until exp.

Planned (v1.2.0)

  • Refresh tokens — a long-lived (e.g. 14 days) refresh token, rotated on every refresh, stored server-side in a refresh_tokens table keyed by jti + user_id. Revocation list lives in the same table (flip revoked_at).
  • Token types — JWTs carry "token_type": "access" | "refresh" so get_current_user rejects refresh tokens at access-protected routes.
  • Brute-force protection — a per-email login rate limit (5/minute) on top of the global IP rate limit.

Rate limiting

  • slowapi with a default of 60/minute. Configurable via rate_limit_per_minute.
  • Token-keyed as of v1.1.0 — key_func derives from the authenticated user when a JWT is present, falling back to get_remote_address for anonymous routes. This avoids the multi-user-behind-NAT false positive.
  • The limiter state is in-process today. Multi-worker deployments need a shared backend; we plan to point slowapi at Redis once the worker queue lands.

CORS

  • Explicit allow-list from settings.allowed_origins (a JSON array in .env).
  • allow_credentials=True, allow_methods=["*"], allow_headers=["*"].
  • No wildcards in defaults. Production must set this to your real origins.

Security headers

Set by the security_headers_middleware in app/main.py for every response:

Header Value
X-Content-Type-Options nosniff
X-Frame-Options DENY
X-XSS-Protection 1; mode=block
Strict-Transport-Security max-age=31536000; includeSubDomains
Referrer-Policy strict-origin-when-cross-origin
Permissions-Policy geolocation=(), microphone=(), camera=()

Note: Permissions-Policy strips microphone access globally; the voice-annotation recorder explicitly requests microphone via getUserMedia, which prompts the user — the policy header doesn't block that, only third-party iframes.

A Content-Security-Policy header is not yet set. Planned: default-src 'self'; img-src 'self' data:; connect-src 'self' https://api.openai.com https://api.deepinfra.com; script-src 'self'; style-src 'self' 'unsafe-inline'.


Secrets handling

  • .env is never committed. The .gitignore covers .env, .env.* (with .env.example and .env.LOCAL_DO_NOT_COMMIT documented exceptions).
  • secret_key is the only env var validated for strength — the Pydantic validator in app/config.py rejects any value < 32 chars or matching the known-weak default.
  • Backend service user should own backend/.env with 0600 permissions.
  • API keys (OpenAI, DeepInfra, Azure DI) are stored in .env only. Rotate any leaked key immediately and regenerate secret_key to invalidate stale tokens.

Request correlation

  • request_id_middleware injects a UUID4 into request.state.request_id for every request and echoes it back as X-Request-ID on the response.
  • The log line emitted per request includes the id: request_id=<uuid> method=POST path=/api/v1/projects/1/review status=201 duration_ms=14.2.
  • File bug reports with the X-Request-ID value and we can grep the full request lifecycle out of the log.

Multi-tenancy

Single-tenant today. The roadmap lives in docs/DATABASE.md → Multi-tenancy roadmap. Summary:

  1. Add organization_id to every domain table.
  2. Embed org_id in JWT claims.
  3. Enforce in app dependencies first, then PostgreSQL Row-Level Security policies once on Postgres.
  4. Append-only audit log keyed by org_id, user_id, request_id, action, entity_type, entity_id.

TRUSTED_HOSTS (Starlette TrustedHostMiddleware) is also planned for this milestone, so the backend rejects Host headers that don't match the configured deployment domain.


Known gaps

  • No CSRF protection. The frontend uses bearer-token auth (not cookies), so CSRF isn't directly applicable, but any future cookie-based flow needs the standard double-submit pattern.
  • No file upload virus scanning. voice_annotations accepts audio blobs by extension only — content sniffing is on the v1.2.0 list (planned dep: python-magic).
  • No PII tagging. Architects' license numbers and project addresses are stored in cleartext. Encrypt-at-rest if your jurisdiction requires it (Postgres TDE or filesystem-level).
  • No SBOM / dep scanning in CI. Planned: pip-audit + npm audit as required-status checks.

Report vulnerabilities privately — please don't open public GitHub issues for security bugs. A SECURITY.md policy file with a contact mailbox is on the v1.2.0 list.

There aren't any published security advisories