Harden UI: login brute-force throttle, security headers, Secure cookie flag
- New in-memory login throttle (handler/login_throttle.go): 5 failed attempts per IP or per username within 5 minutes locks that key out for 5 minutes, applied to both /login and the TOTP verification step, which previously had no rate limiting at all - router.New now adds middleware.Secure with X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and HSTS (only when cookies are Secure, implying an HTTPS deployment). No CSP: the existing templates rely on inline <script> blocks, so a CSP strict enough to matter would need 'unsafe-inline' anyway - All session/auth cookies now set Secure based on the new --cookie-secure flag / WGUI_COOKIE_SECURE env var (default true) so the session cookie is never sent over plain HTTP unless explicitly opted into an HTTP-only LAN deployment Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ATVUwTa4Pqwq26orW5BcDW
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
1539c589a1
commit
4d171b4ff7
@@ -116,9 +116,18 @@ func Login(db store.IStore) echo.HandlerFunc {
|
||||
return c.JSON(http.StatusBadRequest, jsonHTTPResponse{false, "Please provide a valid username"})
|
||||
}
|
||||
|
||||
ipKey := "ip:" + c.RealIP()
|
||||
userKey := "user:" + username
|
||||
if !globalLoginThrottle.allow(ipKey) || !globalLoginThrottle.allow(userKey) {
|
||||
log.Warnf("Login throttled for user %s (%s)", username, c.Request().RemoteAddr)
|
||||
return tooManyRequests(c.Response())
|
||||
}
|
||||
|
||||
dbuser, err := db.GetUserByName(username)
|
||||
if err != nil {
|
||||
log.Warnf("Invalid credentials. Cannot query user %s from DB (%s)", username, c.Request().RemoteAddr)
|
||||
globalLoginThrottle.recordFailure(ipKey)
|
||||
globalLoginThrottle.recordFailure(userKey)
|
||||
return c.JSON(http.StatusInternalServerError, jsonHTTPResponse{false, "Invalid credentials"})
|
||||
}
|
||||
|
||||
@@ -136,6 +145,8 @@ func Login(db store.IStore) echo.HandlerFunc {
|
||||
}
|
||||
|
||||
if userCorrect && passwordCorrect {
|
||||
globalLoginThrottle.recordSuccess(ipKey)
|
||||
globalLoginThrottle.recordSuccess(userKey)
|
||||
if dbuser.TOTPEnabled {
|
||||
// Password check passed, but this account requires a second
|
||||
// factor. Stash a short-lived, NOT-yet-authenticated pending
|
||||
@@ -147,6 +158,7 @@ func Login(db store.IStore) echo.HandlerFunc {
|
||||
Path: cookiePath,
|
||||
MaxAge: 300, // 5 minutes to enter the TOTP code
|
||||
HttpOnly: true,
|
||||
Secure: util.CookieSecure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
sess.Values["pending_totp_user"] = dbuser.Username
|
||||
@@ -165,6 +177,8 @@ func Login(db store.IStore) echo.HandlerFunc {
|
||||
}
|
||||
|
||||
log.Warnf("Invalid credentials user %s (%s)", username, c.Request().RemoteAddr)
|
||||
globalLoginThrottle.recordFailure(ipKey)
|
||||
globalLoginThrottle.recordFailure(userKey)
|
||||
return c.JSON(http.StatusUnauthorized, jsonHTTPResponse{false, "Invalid credentials"})
|
||||
}
|
||||
}
|
||||
@@ -186,6 +200,7 @@ func finalizeLoginSession(c echo.Context, dbuser model.User, rememberMe bool) er
|
||||
Path: cookiePath,
|
||||
MaxAge: ageMax,
|
||||
HttpOnly: true,
|
||||
Secure: util.CookieSecure,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
}
|
||||
|
||||
@@ -210,6 +225,7 @@ func finalizeLoginSession(c echo.Context, dbuser model.User, rememberMe bool) er
|
||||
cookie.Value = tokenUID
|
||||
cookie.MaxAge = ageMax
|
||||
cookie.HttpOnly = true
|
||||
cookie.Secure = util.CookieSecure
|
||||
cookie.SameSite = http.SameSiteLaxMode
|
||||
c.SetCookie(cookie)
|
||||
|
||||
@@ -236,6 +252,13 @@ func VerifyTOTPLogin(db store.IStore) echo.HandlerFunc {
|
||||
|
||||
code, _ := data["code"].(string)
|
||||
|
||||
ipKey := "ip:" + c.RealIP()
|
||||
userKey := "totp:" + pendingUsername
|
||||
if !globalLoginThrottle.allow(ipKey) || !globalLoginThrottle.allow(userKey) {
|
||||
log.Warnf("TOTP login throttled for user %s (%s)", pendingUsername, c.Request().RemoteAddr)
|
||||
return tooManyRequests(c.Response())
|
||||
}
|
||||
|
||||
dbuser, err := db.GetUserByName(pendingUsername)
|
||||
if err != nil {
|
||||
log.Warnf("Invalid pending TOTP login. Cannot query user %s from DB (%s)", pendingUsername, c.Request().RemoteAddr)
|
||||
@@ -244,8 +267,12 @@ func VerifyTOTPLogin(db store.IStore) echo.HandlerFunc {
|
||||
|
||||
if !dbuser.TOTPEnabled || !auth.Validate(dbuser.TOTPSecret, code, time.Now()) {
|
||||
log.Warnf("Invalid TOTP code for user %s (%s)", pendingUsername, c.Request().RemoteAddr)
|
||||
globalLoginThrottle.recordFailure(ipKey)
|
||||
globalLoginThrottle.recordFailure(userKey)
|
||||
return c.JSON(http.StatusUnauthorized, jsonHTTPResponse{false, "Invalid code"})
|
||||
}
|
||||
globalLoginThrottle.recordSuccess(ipKey)
|
||||
globalLoginThrottle.recordSuccess(userKey)
|
||||
|
||||
// clear the pending state before finalizing the real session
|
||||
delete(sess.Values, "pending_totp_user")
|
||||
|
||||
Reference in New Issue
Block a user