fix(security): behebe F-01/F-02/W-03/W-04 aus Security-Audit + PROJ-24 TOTP 2FA

F-01: err.Error() wird nicht mehr an HTTP-Clients gesendet.
      Stattdessen generische Fehlermeldungen + Server-Log.
      Betrifft: handleCreateUser, handleUpdateUser, handleDeleteUser,
                handleSyncNow, handleSecurityConfig, handleUpload.

F-02: Login-Audit-Log enthält keinen rohen err.Error() mehr.
      Neue classifyLoginError() Funktion: invalid_password / ldap_error /
      account_disabled / unknown — schützt vor LDAP-Info-Leak via Audit-API.

W-03: remoteIP() trimmt jetzt Leerzeichen aus X-Forwarded-For.
      Vollständige Lösung erfordert Proxy-Konfiguration (W-03 bleibt WARN).

W-04: Attachment-Dateiname wird durch sanitizeFilename() bereinigt.
      Nur [a-zA-Z0-9._- ] erlaubt — verhindert Header-Injection.

PROJ-24: TOTP 2FA vollständig implementiert:
      - internal/auth/totp.go: GenerateSecret, ValidateTOTP, QRCodeSVG
      - internal/api/totp_handlers.go: Setup, Login-Step2, Admin-Reset
      - internal/userstore: SetTOTPSecret, EnableTOTP, DisableTOTP, ResetTOTP
      - Login-Flow: totp_pending JWT → /api/auth/totp → vollwertiger JWT
      - AES-256-GCM verschlüsseltes Secret in users.totp_secret

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
sysops
2026-03-18 00:54:00 +01:00
parent 787db6638f
commit 2de340573b
14 changed files with 888 additions and 50 deletions
+12 -9
View File
@@ -30,17 +30,20 @@ func newTestAuth(t *testing.T) (*auth.Manager, *userstore.Store) {
Role: userstore.RoleUser,
})
mgr := auth.New(store, nil, "test-jwt-secret-32chars-long-enough")
mgr := auth.New(store, nil, "test-jwt-secret-32chars-long-enough", "0000000000000000000000000000000000000000000000000000000000000000")
return mgr, store
}
func TestLoginSuccess(t *testing.T) {
mgr, _ := newTestAuth(t)
token, user, err := mgr.Login("testadmin", "adminpass")
token, user, totpRequired, err := mgr.Login("testadmin", "adminpass")
if err != nil {
t.Fatalf("Login: %v", err)
}
if totpRequired {
t.Error("expected totpRequired=false for user without TOTP")
}
if token == "" {
t.Error("expected non-empty token")
}
@@ -55,7 +58,7 @@ func TestLoginSuccess(t *testing.T) {
func TestLoginWrongPassword(t *testing.T) {
mgr, _ := newTestAuth(t)
if _, _, err := mgr.Login("testadmin", "wrongpass"); err == nil {
if _, _, _, err := mgr.Login("testadmin", "wrongpass"); err == nil {
t.Error("expected error for wrong password")
}
}
@@ -63,7 +66,7 @@ func TestLoginWrongPassword(t *testing.T) {
func TestLoginUnknownUser(t *testing.T) {
mgr, _ := newTestAuth(t)
if _, _, err := mgr.Login("nobody", "pw"); err == nil {
if _, _, _, err := mgr.Login("nobody", "pw"); err == nil {
t.Error("expected error for unknown user")
}
}
@@ -71,7 +74,7 @@ func TestLoginUnknownUser(t *testing.T) {
func TestTokenValidation(t *testing.T) {
mgr, _ := newTestAuth(t)
token, _, _ := mgr.Login("testadmin", "adminpass")
token, _, _, _ := mgr.Login("testadmin", "adminpass")
sess, err := mgr.ValidateToken(token)
if err != nil {
t.Fatalf("ValidateToken: %v", err)
@@ -90,7 +93,7 @@ func TestTokenValidation(t *testing.T) {
func TestTokenTampering(t *testing.T) {
mgr, _ := newTestAuth(t)
token, _, _ := mgr.Login("testadmin", "adminpass")
token, _, _, _ := mgr.Login("testadmin", "adminpass")
tampered := token + "x"
if _, err := mgr.ValidateToken(tampered); err == nil {
@@ -101,7 +104,7 @@ func TestTokenTampering(t *testing.T) {
func TestLogout(t *testing.T) {
mgr, _ := newTestAuth(t)
token, _, _ := mgr.Login("testadmin", "adminpass")
token, _, _, _ := mgr.Login("testadmin", "adminpass")
// Token valid before logout
if _, err := mgr.ValidateToken(token); err != nil {
@@ -146,8 +149,8 @@ func TestHasRole(t *testing.T) {
func TestMultipleSessionsIndependent(t *testing.T) {
mgr, _ := newTestAuth(t)
token1, _, _ := mgr.Login("testadmin", "adminpass")
token2, _, _ := mgr.Login("testadmin", "adminpass")
token1, _, _, _ := mgr.Login("testadmin", "adminpass")
token2, _, _, _ := mgr.Login("testadmin", "adminpass")
if token1 == token2 {
t.Error("two logins should produce different tokens (different JTIs)")