From fdb18300d91e501e800169e4451eea5b37b14c35 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Mon, 1 Jun 2026 14:31:21 +0200 Subject: [PATCH] docs(02): add code review report for plan 06 gap closure --- .../02-users-authentication/02-REVIEW.md | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 .planning/phases/02-users-authentication/02-REVIEW.md diff --git a/.planning/phases/02-users-authentication/02-REVIEW.md b/.planning/phases/02-users-authentication/02-REVIEW.md new file mode 100644 index 0000000..00ebdbe --- /dev/null +++ b/.planning/phases/02-users-authentication/02-REVIEW.md @@ -0,0 +1,187 @@ +--- +phase: 02-users-authentication +reviewed: 2026-06-01T00:00:00Z +depth: standard +files_reviewed: 6 +files_reviewed_list: + - frontend/src/components/settings/SettingsAccountTab.vue + - frontend/src/App.vue + - frontend/src/router/index.js + - frontend/src/views/SettingsView.vue + - frontend/src/components/auth/TotpEnrollment.vue + - frontend/package.json +findings: + critical: 3 + warning: 4 + info: 1 + total: 8 +status: issues_found +--- + +# Phase 02: Code Review Report + +**Reviewed:** 2026-06-01T00:00:00Z +**Depth:** standard +**Files Reviewed:** 6 +**Status:** issues_found + +## Summary + +The gap-closure plan (02-06) wires up layout switching, auth route guards, and the TOTP enrollment QR improvement. The layout-aware App.vue and the `meta.layout='auth'` pattern are sound. The navigation guard correctly uses the `!to.meta.public` predicate to cover all authenticated routes — including those that carry no explicit `meta.requiresAuth` flag (/, /topics, /settings, etc.) — and deduplicates concurrent refresh calls via `_refreshInFlight`. No XSS vectors were found; Vue text interpolation auto-escapes all user-controlled values throughout. + +Three blockers were found: the backend `change_password` and `disable_totp` endpoints do not revoke active sessions as required by CLAUDE.md line 153, and a dead `#confirm-button` slot in `SettingsAccountTab.vue` silently discards the spinner/disabled guard on "Sign out all devices," allowing double-invocation with no visual feedback. Four warnings cover a URIError crash path in `onMounted`, a TOTP re-submission window after successful verification, fragile string-matching for password error routing, and an unconditional authenticated API call on every auth page load. + +## Critical Issues + +### CR-01: Password change does not revoke active sessions (backend, auth.py) + +**File:** `backend/api/auth.py:520` +**Issue:** CLAUDE.md line 153 is explicit: "Password change, TOTP enroll/revoke, and account deactivation immediately revoke all active sessions." The `change_password` endpoint updates `password_hash` and commits but performs no refresh-token revocation. An attacker who obtained a valid refresh cookie before the password change retains full access until their token naturally expires (up to 30 days). The frontend `SettingsAccountTab.vue` also makes no attempt to trigger session revocation after a successful password change. +**Fix:** +```python +# After session.commit() in change_password, revoke all refresh tokens for the user: +await session.execute( + delete(RefreshToken).where(RefreshToken.user_id == current_user.id) +) +await session.commit() +``` +Also add a `router.push('/login')` (or `authStore.logout()`) call in the frontend `changePassword()` success path so the current session is visibly invalidated. + +--- + +### CR-02: TOTP disable does not revoke active sessions (backend, auth.py) + +**File:** `backend/api/auth.py:641` +**Issue:** Same CLAUDE.md line 153 requirement applies to TOTP revoke. The `disable_totp` endpoint clears `totp_secret` / `totp_enabled` and deletes backup codes but does not revoke any refresh tokens. An attacker with a hijacked session can downgrade a victim's account security by removing 2FA, and both the attacker and any existing stolen sessions remain valid afterward. +**Fix:** +```python +# After deleting backup codes in disable_totp, revoke all refresh tokens: +await session.execute( + delete(RefreshToken).where(RefreshToken.user_id == current_user.id) +) +await session.commit() +``` + +--- + +### CR-03: Dead `#confirm-button` slot removes spinner and disabled guard from "Sign out all devices" + +**File:** `frontend/src/components/settings/SettingsAccountTab.vue:149-161` +**Issue:** `ConfirmBlock.vue` defines no named slot — it has a single hard-coded `