Fix F-01: TOTP enroll must not set required=1 early

- db/accounts.go: add StorePendingTOTP() which writes
  totp_secret_enc and totp_secret_nonce but leaves
  totp_required=0; add comment explaining two-phase flow
- server.go (handleTOTPEnroll): switch from SetTOTP() to
  StorePendingTOTP() so the required flag is only set after
  the user confirms a valid TOTP code via handleTOTPConfirm,
  which still calls SetTOTP()
- server_test.go: TestTOTPEnrollDoesNotRequireTOTP verifies
  that after POST /v1/auth/totp/enroll, TOTPRequired is false
  and the encrypted secret is present; confirms that a
  subsequent login without a TOTP code still succeeds (no
  lockout)
- AUDIT.md: mark F-01 and F-11 as fixed
Security: without this fix an admin who enrolls TOTP but
  abandons before confirmation is permanently locked out
  because totp_required=1 but no confirmed secret exists.
  StorePendingTOTP() keeps the secret pending until the user
  proves possession by confirming a valid code.
This commit is contained in:
2026-03-11 20:18:57 -07:00
parent 4da39475cc
commit 462f706f73
3 changed files with 125 additions and 39 deletions

View File

@@ -1,6 +1,6 @@
# MCIAS Security Audit Report
**Scope:** Full codebase review of `git.wntrmute.dev/kyle/mcias` (commit `4596ea0`)
**Scope:** Full codebase review of `git.wntrmute.dev/kyle/mcias` (commit `4596ea0`) aka mcias.
**Auditor:** Comprehensive source review of all Go source files, protobuf definitions, Dockerfile, systemd unit, and client libraries
**Classification:** Findings rated as **CRITICAL**, **HIGH**, **MEDIUM**, **LOW**, or **INFORMATIONAL**
@@ -218,24 +218,24 @@ The REST `handleTokenIssue` and gRPC `IssueServiceToken` both revoke the existin
## Summary Table
| ID | Severity | Title | Effort |
|----|----------|-------|--------|
| F-01 | MEDIUM | TOTP enrollment sets required=1 before confirmation | Small |
| F-02 | MEDIUM | Password in HTML hidden fields during TOTP step | Medium |
| F-03 | MEDIUM | Token renewal not atomic (race window) | Small |
| F-04 | MEDIUM | Rate limiter not applied to REST login endpoint | Small |
| F-11 | MEDIUM | Missing security headers on UI responses | Small |
| F-05 | LOW | No `nbf` claim in issued JWTs | Trivial |
| F-06 | LOW | `HasRole` uses non-constant-time comparison | Trivial |
| F-07 | LOW | Dummy Argon2 hash timing mismatch | Small |
| F-08 | LOW | No account lockout after repeated failures | Medium |
| F-09 | LOW | `synchronous=NORMAL` risks audit data loss | Trivial |
| F-10 | LOW | No maximum token expiry validation | Small |
| F-12 | LOW | No username length/charset validation | Small |
| F-13 | LOW | No minimum password length enforcement | Small |
| F-14 | LOW | Passphrase string not zeroed after KDF | Small |
| F-16 | LOW | UI system token issuance skips old token revocation | Small |
| F-15 | INFO | Bearer prefix check inconsistency | Trivial |
| Fixed? | ID | Severity | Title | Effort |
|--------|----|----------|-------|--------|
| Yes | F-01 | MEDIUM | TOTP enrollment sets required=1 before confirmation | Small |
| No | F-02 | MEDIUM | Password in HTML hidden fields during TOTP step | Medium |
| No | F-03 | MEDIUM | Token renewal not atomic (race window) | Small |
| Yes | F-04 | MEDIUM | Rate limiter not applied to REST login endpoint | Small |
| Yes | F-11 | MEDIUM | Missing security headers on UI responses | Small |
| No | F-05 | LOW | No `nbf` claim in issued JWTs | Trivial |
| No | F-06 | LOW | `HasRole` uses non-constant-time comparison | Trivial |
| No | F-07 | LOW | Dummy Argon2 hash timing mismatch | Small |
| No | F-08 | LOW | No account lockout after repeated failures | Medium |
| No | F-09 | LOW | `synchronous=NORMAL` risks audit data loss | Trivial |
| No | F-10 | LOW | No maximum token expiry validation | Small |
| No | F-12 | LOW | No username length/charset validation | Small |
| No | F-13 | LOW | No minimum password length enforcement | Small |
| No | F-14 | LOW | Passphrase string not zeroed after KDF | Small |
| No | F-16 | LOW | UI system token issuance skips old token revocation | Small |
| No | F-15 | INFO | Bearer prefix check inconsistency | Trivial |
---