Files
kite/.planning/phases/02-users-authentication/02-SECURITY.md
T
2026-06-01 14:55:15 +02:00

11 KiB

phase, slug, status, threats_open, asvs_level, created
phase slug status threats_open asvs_level created
2 users-authentication verified 0 L2 2026-06-01

Phase 2 — Security

Per-phase security contract: threat register, accepted risks, and audit trail.


Trust Boundaries

Boundary Description Data Crossing
client→API (auth service) Untrusted email, password, handle, totp_code, backup_code in JSON body Credentials / PII
API→Redis (rate limiter + replay) IP-keyed/email-keyed counters + TOTP replay keys written/read Opaque rate counters, used-code markers
API→HIBP external SHA-1 prefix (5 chars) of password sent to third-party Anonymised password hash fragment
FastAPI→browser (cookies) httpOnly refresh token cookie Short-lived session credential
admin JWT→API (admin endpoints) Admin Bearer token verified on every request Role-restricted metadata
admin→user data Admin reads user metadata; must never see document content or credentials User PII (whitelisted only)
router guard Unauthenticated or non-admin client navigates to /admin Route meta, role claim
layout selection Auth pages must not render app shell leaking user identity Sidebar / session info

Threat Register

Threat ID STRIDE Component Disposition Mitigation Status Evidence
T-02-01 Spoofing JWT decode typ claim mitigate payload.get("typ") != "access" raises ValueError — prevents reset tokens used as access tokens CLOSED services/auth.py:93
T-02-02 Spoofing Refresh token reuse mitigate Family revocation: all tokens for user_id revoked + security alert email on reuse CLOSED services/auth.py:181-185
T-02-03 Tampering Backup code storage mitigate Argon2 hash stored; constant-time verify_password() compare CLOSED services/auth.py:310,338
T-02-04 Repudiation bootstrap_admin idempotency mitigate select(User).limit(1) guard before insert; WARNING log when env vars absent CLOSED services/auth.py:397-408
T-02-05 Info Disclosure HIBP k-anonymity mitigate SHA-1[:5] prefix only sent; suffix compared locally via hmac.compare_digest CLOSED services/auth.py:360
T-02-06 DoS HIBP network call accept Fail-open (return False), httpx timeout=5s, warning logged — see Accepted Risks CLOSED services/auth.py:369-371
T-02-07 EoP get_current_admin mitigate if user.role != "admin": raise HTTPException(403) CLOSED deps/auth.py:87
T-02-08 EoP Admin impersonation exclusion mitigate Architectural exclusion — zero impersonation endpoints; AST confirmed CLOSED api/admin.py (0 grep hits)
T-02-SC Tampering Supply chain (PyJWT/pwdlib/pyotp/slowapi) mitigate All packages pinned in requirements.txt; legitimacy verified at plan time CLOSED backend/requirements.txt:23-26
T-02-09 Spoofing Login email enumeration mitigate Identical "Incorrect email or password" for non-existent email and wrong password CLOSED api/auth.py:248
T-02-10 Spoofing Password reset email enumeration mitigate 202 returned unconditionally — outside if user is not None block CLOSED api/auth.py:648,673
T-02-11 Tampering CSRF mitigate samesite="strict" on refresh cookie + OriginValidationMiddleware rejects foreign origins CLOSED api/auth.py:100, main.py:47-61
T-02-12 Info Disclosure Access token in JavaScript accept Pinia ref(null) only; zero localStorage/sessionStorage writes — see Accepted Risks CLOSED stores/auth.js
T-02-13 DoS Login/register rate limiting mitigate @limiter.limit("10/minute") on /login, /register, /refresh + per-account Redis counter 10/15min CLOSED api/auth.py:121,195,326,215-224
T-02-14 Info Disclosure Security headers missing mitigate SecurityHeadersMiddleware sets CSP, X-Frame-Options: DENY, X-Content-Type-Options: nosniff CLOSED main.py:32-40
T-02-15 Tampering CORS wildcard mitigate allow_origins=settings.cors_origins — wildcard removed CLOSED main.py:124
T-02-16 EoP password_must_change bypass mitigate /login returns 200 {requires_password_change: true} with no tokens when flag set CLOSED api/auth.py:259-260
T-02-17 Spoofing TOTP replay mitigate Redis key totp_used:{user_id}:{code} pre-checked; written with ex=90 (s) CLOSED services/auth.py:262-270
T-02-18 Spoofing Backup code reuse mitigate BackupCode.used_at.is_(None) filter; used_at = now() on first use CLOSED services/auth.py:330,345
T-02-19 Info Disclosure Backup codes one-time exposure mitigate Plaintext returned once from /totp/enable only; DB stores Argon2 hashes CLOSED api/auth.py:594-609
T-02-20 EoP Password reset token type confusion mitigate decode_password_reset_token validates typ="password-reset" CLOSED services/auth.py:125-126
T-02-21 EoP Password reset auto-login mitigate Confirm endpoint returns {"message": "..."} only — no access_token key CLOSED api/auth.py:730
T-02-22 Info Disclosure Email enumeration via password reset mitigate HTTP 202 returned unconditionally, outside if user is not None block CLOSED api/auth.py:673
T-02-23 Tampering TOTP constant-time compare accept pyotp compare negligible for 6-digit codes; 10/min rate limit is primary defence — see Accepted Risks CLOSED api/auth.py:565
T-02-24 Spoofing Sign-out-all confirmation mitigate ConfirmBlock.vue explicit confirmed emit; AccountView wires @confirmedlogoutAll() CLOSED ConfirmBlock.vue
T-02-25 DoS TOTP brute force mitigate @limiter.limit("10/minute") on POST /totp/enable CLOSED api/auth.py:565
T-02-26A EoP Admin endpoints without role check mitigate get_current_admin Depends() on all 12 handlers in admin.py CLOSED api/admin.py (grep count = 12)
T-02-26B Spoofing Backup code reuse at login mitigate verify_backup_code() sets used_at; subsequent calls always return False CLOSED services/auth.py:330
T-02-27A Info Disclosure Admin user list sensitive fields mitigate _user_to_dict() whitelist — password_hash, credentials_enc, totp_secret absent CLOSED api/admin.py:75-90
T-02-27B Spoofing Backup code brute force at login mitigate Per-account Redis counter incremented before TOTP/backup_code branch — covers all login paths CLOSED api/auth.py:215-224
T-02-28 EoP Admin impersonation (no endpoint) mitigate Zero grep matches for impersonation strings; test_admin_impersonation_not_found asserts 404 CLOSED api/admin.py
T-02-29 DoS Admin deactivating all admins mitigate active_admin_count <= 1 guard; raises HTTP 400 before deactivation CLOSED api/admin.py:305-316
T-02-30A Tampering Admin password reset grants admin access mitigate HTTP 202 + message only; reset token emailed to user's inbox; never in response body CLOSED api/admin.py:348,377
T-02-30B EoP Admin link visible to non-admin mitigate v-if="authStore.user?.role === 'admin'" on sidebar link CLOSED AppSidebar.vue:189
T-02-31A Info Disclosure Quota endpoint exposes storage accept Admin operational data — no PII, no document content — see Accepted Risks CLOSED api/admin.py
T-02-31B EoP Admin UI impersonation mitigate All three admin tab components contain zero impersonation UI strings CLOSED Admin components (0 grep hits)
T-02-32A EoP Admin-created user skips password change mitigate password_must_change=True set in User constructor on POST /api/admin/users CLOSED api/admin.py:255
T-02-32B Info Disclosure Admin panel renders sensitive data mitigate AdminUsersTab.vue binds safe fields only; zero password_hash/credentials_enc in template CLOSED AdminUsersTab.vue
T-02-33 Tampering Inline deactivation without confirmation mitigate confirmDeactivate === user.id inline block shows email before API call CLOSED AdminUsersTab.vue:153-174
T-02-34 DoS Admin creates unlimited users accept Admin is trusted role; single-tenant deployment — see Accepted Risks CLOSED intentional
T-02-GAP-01 EoP Router beforeEach admin guard mitigate requiresAdmin meta + role check; non-admin redirected to / CLOSED router/index.js:42,91-93
T-02-GAP-02 Info Disclosure AppSidebar on auth routes mitigate <AuthLayout v-if="route.meta.layout === 'auth'"> — sidebar absent on all public routes CLOSED App.vue:2
T-02-GAP-03 Tampering admin.py create_user flush order accept await session.flush() present before write_audit_log(); regression test confirms — see Accepted Risks CLOSED api/admin.py:265
T-02-GAP-SC Tampering npm qrcode supply chain mitigate qrcode@^1.5.4 canonical package (20M+/week downloads); verified at plan time CLOSED frontend/package.json:13

Status: open · closed Disposition: mitigate (implementation required) · accept (documented risk) · transfer (third-party)


Accepted Risks Log

Risk ID Threat Ref Rationale Accepted By Date
AR-02-01 T-02-06 HIBP network errors fail-open to keep registration/login available; warning logged; auth proceeds. Downside: a pwned password might slip through during HIBP outage. Risk: LOW — outages are rare and short. GSD planner 2026-06-01
AR-02-02 T-02-12 Access token stored in Pinia ref() (in-memory) only — lost on page refresh, requiring silent refresh flow. Alternative (localStorage) would introduce XSS extraction risk rated HIGHER. GSD planner 2026-06-01
AR-02-03 T-02-23 pyotp verify() uses Python string comparison on 6-digit numeric codes. Timing difference is negligible and unexploitable at this granularity. Rate limiting (10/min) is the primary brute-force control. GSD planner 2026-06-01
AR-02-04 T-02-31A Quota endpoint (GET /api/admin/users/{id}/quota) exposes limit_bytes / used_bytes. These are operational metrics — no PII, no document content, no credentials. Acceptable admin-visible data. GSD planner 2026-06-01
AR-02-05 T-02-34 Admin user creation has no rate limit. Admin is an explicitly trusted role. Unlimited user creation is intentional for single-tenant deployments where the admin is the operator. GSD planner 2026-06-01
AR-02-06 T-02-GAP-03 session.flush() ordering in create_user was flagged as a potential FK race. Confirmed resolved: await session.flush() precedes write_audit_log(); regression test test_create_user_sets_password_must_change covers the ordering. GSD planner 2026-06-01

Security Audit Trail

Audit Date Threats Total Closed Open Run By
2026-06-01 43 43 0 gsd-security-auditor (claude-sonnet-4-6)

Sign-Off

  • All threats have a disposition (mitigate / accept / transfer)
  • Accepted risks documented in Accepted Risks Log
  • threats_open: 0 confirmed
  • status: verified set in frontmatter

Approval: verified 2026-06-01