Files
mcias/AUDIT.md
Kyle Isom 2a85d4bf2b Update AUDIT.md: all SEC findings remediated
- Mark SEC-01 through SEC-12 as fixed with fix descriptions
- Update executive summary to reflect full remediation
- Move original finding descriptions to collapsible section
- Replace remediation priority table with status section

Security: documentation-only change, no code modifications

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-14 21:31:30 -07:00

10 KiB

MCIAS Security Audit Report

Date: 2026-03-14 (updated — all findings remediated) Original audit date: 2026-03-13 Auditor role: Penetration tester (code review + live instance probing) Scope: Full codebase and running instance at localhost:8443 — authentication flows, token lifecycle, cryptography, database layer, REST/gRPC/UI servers, authorization, headers, and operational security. Methodology: Static code analysis, live HTTP probing, architectural review.


Executive Summary

MCIAS has a strong security posture. All findings from three audit rounds (CRIT-01/CRIT-02, DEF-01 through DEF-10, and SEC-01 through SEC-12) have been remediated. The cryptographic foundations are sound, JWT validation is correct, SQL injection is not possible, XSS is prevented by Go's html/template auto-escaping, and CSRF protection is well-implemented.

All findings from this audit have been remediated. See the remediation table below for details.


Remediated Findings (SEC-01 through SEC-12)

All findings from this audit have been remediated. The original descriptions are preserved below for reference.

ID Severity Finding Status
SEC-01 Medium TOTP enrollment did not require password re-authentication Fixed — both REST and gRPC now require current password, with lockout counter on failure
SEC-02 Medium Account lockout response leaked account existence Fixed — locked accounts now return same 401 "invalid credentials" as wrong password, with dummy Argon2 for timing uniformity
SEC-03 Medium Token renewal had no proximity or re-auth check Fixed — renewal requires token to have consumed ≥50% of its lifetime
SEC-04 Low-Med REST API responses lacked security headers FixedglobalSecurityHeaders middleware applies X-Content-Type-Options, HSTS, and Cache-Control: no-store to all routes
SEC-05 Low No request body size limit on REST API FixeddecodeJSON wraps body with http.MaxBytesReader (1 MiB); max password length enforced
SEC-06 Low gRPC rate limiter ignored TrustedProxy FixedgrpcClientIP extracts real client IP via metadata when peer matches trusted proxy
SEC-07 Low Static file directory listing enabled FixednoDirListing wrapper returns 404 for directory requests
SEC-08 Low System token issuance was not atomic FixedIssueSystemToken wraps revoke+track in a single SQLite transaction
SEC-09 Info Navigation bar exposed admin UI structure to non-admin users Fixed — nav links conditionally rendered with {{if .IsAdmin}}
SEC-10 Info No Permissions-Policy header FixedPermissions-Policy: camera=(), microphone=(), geolocation=(), payment=() added
SEC-11 Info Audit log details used fmt.Sprintf instead of json.Marshal Fixedaudit.JSON and audit.JSONWithRoles helpers use json.Marshal
SEC-12 Info Default token expiry was 30 days Fixed — default reduced to 7 days (168h); renewal proximity check (SEC-03) further limits exposure
Original finding descriptions (click to expand)

SEC-01 — TOTP Enrollment Does Not Require Password Re-authentication (Medium)

Files: internal/server/server.go, internal/grpcserver/auth.go

POST /v1/auth/totp/enroll and the gRPC EnrollTOTP RPC originally required only a valid JWT — no password confirmation. If an attacker stole a session token, they could enroll TOTP on the victim's account.

Fix: Both endpoints now require the current password, with lockout counter incremented on failure.


SEC-02 — Account Lockout Response Leaks Account Existence (Medium)

Locked accounts originally returned HTTP 429 / gRPC ResourceExhausted with "account temporarily locked", distinguishable from the HTTP 401 "invalid credentials" returned for wrong passwords.

Fix: All login paths now return the same "invalid credentials" response for locked accounts, with dummy Argon2 to maintain timing uniformity.


SEC-03 — Token Renewal Has No Proximity or Re-auth Check (Medium)

POST /v1/auth/renew originally accepted any valid token regardless of remaining lifetime.

Fix: Renewal now requires the token to have consumed ≥50% of its lifetime before it can be renewed.


SEC-04 — REST API Responses Lack Security Headers (Low-Medium)

API endpoints originally returned only Content-Type — no Cache-Control, X-Content-Type-Options, or HSTS.

Fix: globalSecurityHeaders middleware applies these headers to all routes (API and UI).


SEC-05 — No Request Body Size Limit on REST API Endpoints (Low)

decodeJSON originally read from r.Body without any size limit.

Fix: http.MaxBytesReader with 1 MiB limit added to decodeJSON. Maximum password length also enforced.


SEC-06 — gRPC Rate Limiter Ignores TrustedProxy (Low)

The gRPC rate limiter originally used peer.FromContext directly, always getting the proxy IP behind a reverse proxy.

Fix: grpcClientIP now reads from gRPC metadata headers when the peer matches the trusted proxy.


SEC-07 — Static File Directory Listing Enabled (Low)

http.FileServerFS served directory listings by default.

Fix: noDirListing wrapper returns 404 for directory requests.


SEC-08 — System Token Issuance Is Not Atomic (Low)

handleTokenIssue originally performed three sequential non-transactional operations.

Fix: IssueSystemToken wraps all operations in a single SQLite transaction.


SEC-09 — Navigation Bar Exposes Admin UI Structure to Non-Admin Users (Informational)

Nav links were rendered for all authenticated users.

Fix: Admin nav links wrapped in {{if .IsAdmin}} conditional.


SEC-10 — No Permissions-Policy Header (Informational)

The security headers middleware did not include Permissions-Policy.

Fix: Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=() added.


SEC-11 — Audit Log Details Use fmt.Sprintf Instead of json.Marshal (Informational)

Audit details were constructed with fmt.Sprintf and %q, which is fragile for JSON.

Fix: audit.JSON and audit.JSONWithRoles helpers use json.Marshal.


SEC-12 — Default Token Expiry Is 30 Days (Informational / Configuration)

Default expiry was 720h (30 days).

Fix: Reduced to 168h (7 days). Combined with SEC-03's renewal proximity check, exposure window is significantly reduced.


Previously Remediated Findings (CRIT/DEF series)

The following findings from the initial audit (2026-03-12) were confirmed fixed in the 2026-03-13 audit:

ID Finding Status
CRIT-01 TOTP replay attack — no counter tracking FixedCheckAndUpdateTOTPCounter with atomic SQL, migration 000007
CRIT-02 gRPC EnrollTOTP called SetTOTP instead of StorePendingTOTP Fixed — now calls StorePendingTOTP
DEF-01 No rate limiting on UI login FixedloginRateLimit applied to POST /login
DEF-02 pendingLogins map had no expiry cleanup FixedcleanupPendingLogins goroutine runs every 5 minutes
DEF-03 Rate limiter ignored X-Forwarded-For FixedClientIP() respects TrustedProxy config
DEF-04 Missing nbf claim on tokens FixedNotBefore: jwt.NewNumericDate(now) added
DEF-05 No max token expiry ceiling Fixed — upper bounds enforced in config validation
DEF-06 Incorrect case-sensitivity comment Fixed — comment corrected
DEF-07 SQLite synchronous=NORMAL Fixed — changed to PRAGMA synchronous=FULL
DEF-08 gRPC counted TOTP-missing as failure Fixed — no longer increments lockout counter
DEF-09 Security headers missing on docs endpoints FixeddocsSecurityHeaders wrapper added
DEF-10 Role strings not validated Fixedmodel.ValidateRole() with compile-time allowlist

Positive Findings (Preserved)

These implementation details are exemplary and should be maintained:

Area Detail
JWT alg confusion ValidateToken enforces alg=EdDSA in the key function before signature verification
Constant-time operations crypto/subtle.ConstantTimeCompare for password hashes, CSRF tokens; all three TOTP windows evaluated without early exit
Timing uniformity Dummy Argon2 via sync.Once for unknown/inactive users on all login paths
Token revocation Fail-closed: untracked tokens are rejected, not silently accepted
Token renewal atomicity RenewToken wraps revoke+track in a single SQLite transaction
TOTP replay prevention Counter-based replay detection with atomic SQL UPDATE/WHERE
TOTP nonce design 128-bit single-use server-side nonce; password never retransmitted in step 2
CSRF protection HMAC-SHA256 double-submit cookie, domain-separated key derivation, SameSite=Strict, constant-time validation
Credential exclusion json:"-" on all credential fields; password hash never in API responses
Security headers (UI) CSP (no unsafe-inline), X-Content-Type-Options, X-Frame-Options DENY, HSTS 2yr, Referrer-Policy no-referrer
Cookie hardening HttpOnly + Secure + SameSite=Strict on session cookie
Account lockout 10-attempt rolling window, checked before Argon2, with timing-safe dummy hash
Argon2id parameters Config validator enforces OWASP 2023 minimums; rejects weakening
SQL injection Zero string concatenation — all queries parameterized
Input validation Username regex + length, password min length, account type enum, role allowlist, JSON strict decoder
Audit logging Append-only, no delete path, credentials never logged, actor/target/IP captured
Master key hygiene Env var cleared after read, key zeroed on shutdown, AES-256-GCM at rest
TLS MinVersion TLS 1.2, X25519 preferred, no plaintext listener, read/write/idle timeouts set

Remediation Status

All findings remediated. No open items remain. Next audit should focus on:

  • Any new features added since 2026-03-14
  • Dependency updates and CVE review
  • Live penetration testing of remediated endpoints