| 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 @confirmed → logoutAll() |
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 |