Fix SEC-11: use json.Marshal for audit details
- Add internal/audit package with JSON() and JSONWithRoles() helpers
that use json.Marshal instead of fmt.Sprintf with %q
- Replace all fmt.Sprintf audit detail construction in:
- internal/server/server.go (10 occurrences)
- internal/ui/handlers_auth.go (4 occurrences)
- internal/grpcserver/auth.go (4 occurrences)
- Add tests for the helpers including edge-case Unicode,
null bytes, special characters, and odd argument counts
- Fix broken {"roles":%v} formatting that produced invalid JSON
Security: Audit log detail strings are now constructed via
json.Marshal, which correctly handles all Unicode edge cases
(U+2028, U+2029, null bytes, etc.) that fmt.Sprintf with %q
may mishandle. This prevents potential log injection or parsing
issues in audit event consumers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"git.wntrmute.dev/kyle/mcias/internal/audit"
|
||||
"git.wntrmute.dev/kyle/mcias/internal/auth"
|
||||
"git.wntrmute.dev/kyle/mcias/internal/crypto"
|
||||
"git.wntrmute.dev/kyle/mcias/internal/model"
|
||||
@@ -59,7 +59,7 @@ func (u *UIServer) handleLoginPost(w http.ResponseWriter, r *http.Request) {
|
||||
// Security: always run dummy Argon2 to prevent timing-based user enumeration.
|
||||
_, _ = auth.VerifyPassword("dummy", u.dummyHash())
|
||||
u.writeAudit(r, model.EventLoginFail, nil, nil,
|
||||
fmt.Sprintf(`{"username":%q,"reason":"unknown_user"}`, username))
|
||||
audit.JSON("username", username, "reason", "unknown_user"))
|
||||
u.render(w, "login", LoginData{Error: "invalid credentials"})
|
||||
return
|
||||
}
|
||||
@@ -130,7 +130,7 @@ func (u *UIServer) handleTOTPStep(w http.ResponseWriter, r *http.Request) {
|
||||
accountID, ok := u.consumeTOTPNonce(nonce)
|
||||
if !ok {
|
||||
u.writeAudit(r, model.EventLoginFail, nil, nil,
|
||||
fmt.Sprintf(`{"username":%q,"reason":"invalid_totp_nonce"}`, username))
|
||||
audit.JSON("username", username, "reason", "invalid_totp_nonce"))
|
||||
u.render(w, "login", LoginData{Error: "session expired, please log in again"})
|
||||
return
|
||||
}
|
||||
@@ -238,7 +238,7 @@ func (u *UIServer) finishLogin(w http.ResponseWriter, r *http.Request, acct *mod
|
||||
|
||||
u.writeAudit(r, model.EventLoginOK, &acct.ID, nil, "")
|
||||
u.writeAudit(r, model.EventTokenIssued, &acct.ID, nil,
|
||||
fmt.Sprintf(`{"jti":%q,"via":"ui"}`, claims.JTI))
|
||||
audit.JSON("jti", claims.JTI, "via", "ui"))
|
||||
|
||||
// Redirect to dashboard.
|
||||
if isHTMX(r) {
|
||||
@@ -259,7 +259,7 @@ func (u *UIServer) handleLogout(w http.ResponseWriter, r *http.Request) {
|
||||
u.logger.Warn("revoke token on UI logout", "error", revokeErr)
|
||||
}
|
||||
u.writeAudit(r, model.EventTokenRevoked, nil, nil,
|
||||
fmt.Sprintf(`{"jti":%q,"reason":"ui_logout"}`, claims.JTI))
|
||||
audit.JSON("jti", claims.JTI, "reason", "ui_logout"))
|
||||
}
|
||||
}
|
||||
u.clearSessionCookie(w)
|
||||
|
||||
Reference in New Issue
Block a user