feat(security): add trusted proxies and abuse event cleanup
All checks were successful
Build and Publish Docker Image / deploy (push) Successful in 1m38s

- Add `WARPBOX_TRUSTED_PROXIES` configuration to restrict accepted forwarded client IP headers to specific proxy IPs/CIDRs, securing client IP resolution.
- Integrate `BanService` into the background cleanup job to automatically purge expired abuse and ban evidence events.
- Update documentation with reverse proxy security guidelines and a production systemd deployment guide.
This commit is contained in:
2026-05-31 21:52:56 +03:00
parent 2d04a42736
commit 10ed806153
38 changed files with 2310 additions and 43 deletions

View File

@@ -35,6 +35,7 @@ func (a *App) Register(w http.ResponseWriter, r *http.Request) {
func (a *App) RegisterPost(w http.ResponseWriter, r *http.Request) {
if !a.rateLimiter.Allow("register:"+uploadClientIP(r), 10, time.Minute, time.Now().UTC()) {
a.logger.Warn("registration rate limited", "source", "auth", "severity", "warn", "code", 4291, "ip", uploadClientIP(r))
a.renderAuth(w, r, http.StatusTooManyRequests, authPageData{Mode: "register", Error: "Too many registration attempts."})
return
}
@@ -44,10 +45,11 @@ func (a *App) RegisterPost(w http.ResponseWriter, r *http.Request) {
}
user, err := a.authService.CreateBootstrapUser(r.FormValue("username"), r.FormValue("email"), r.FormValue("password"))
if err != nil {
a.logger.Warn("bootstrap registration failed", "source", "auth", "severity", "warn", "code", 4400, "ip", uploadClientIP(r), "email", r.FormValue("email"), "error", err.Error())
a.renderAuth(w, r, http.StatusBadRequest, authPageData{Mode: "register", Error: err.Error()})
return
}
a.logger.Info("first admin created", "source", "auth", "severity", "user_activity", "code", 2401, "user_id", user.ID)
a.logger.Info("first admin created", "source", "auth", "severity", "user_activity", "code", 2401, "user_id", user.ID, "ip", uploadClientIP(r))
a.loginAndRedirect(w, r, user.Email, r.FormValue("password"), "/app")
}
@@ -61,6 +63,7 @@ func (a *App) Login(w http.ResponseWriter, r *http.Request) {
func (a *App) LoginPost(w http.ResponseWriter, r *http.Request) {
if !a.rateLimiter.Allow("login:"+uploadClientIP(r), 10, time.Minute, time.Now().UTC()) {
a.logger.Warn("login rate limited", "source", "auth", "severity", "warn", "code", 4292, "ip", uploadClientIP(r), "email", r.FormValue("email"))
a.renderAuth(w, r, http.StatusTooManyRequests, authPageData{Mode: "login", Error: "Too many login attempts."})
return
}
@@ -74,12 +77,13 @@ func (a *App) LoginPost(w http.ResponseWriter, r *http.Request) {
}
user, token, err := a.authService.Login(r.FormValue("email"), r.FormValue("password"))
if err != nil {
a.logger.Warn("login failed", "source", "auth", "severity", "warn", "code", 4401, "email", r.FormValue("email"))
a.logger.Warn("login failed", "source", "auth", "severity", "warn", "code", 4401, "email", r.FormValue("email"), "ip", uploadClientIP(r))
a.recordLoginAbuse(r, services.AbuseKindUserLogin, "user login failed")
a.renderAuth(w, r, http.StatusUnauthorized, authPageData{Mode: "login", Error: "Invalid email or password.", ReturnPath: next})
return
}
a.setUserSessionCookie(w, r, token)
a.logger.Info("user login", "source", "auth", "severity", "user_activity", "code", 2402, "user_id", user.ID)
a.logger.Info("user login", "source", "auth", "severity", "user_activity", "code", 2402, "user_id", user.ID, "ip", uploadClientIP(r))
http.Redirect(w, r, safeReturnPath(next), http.StatusSeeOther)
}
@@ -87,6 +91,9 @@ func (a *App) Logout(w http.ResponseWriter, r *http.Request) {
if !a.validateCSRF(w, r) {
return
}
if user, ok := a.currentUser(r); ok {
a.logger.Info("user logout", "source", "auth", "severity", "user_activity", "code", 2405, "user_id", user.ID, "ip", uploadClientIP(r))
}
if cookie, err := r.Cookie(userSessionCookieName); err == nil {
_ = a.authService.Logout(cookie.Value)
}
@@ -107,6 +114,7 @@ func (a *App) InvitePost(w http.ResponseWriter, r *http.Request) {
token := r.PathValue("token")
invite, err := a.authService.InviteByToken(token)
if err != nil {
a.logger.Warn("invite accept invalid", "source", "auth", "severity", "warn", "code", 4404, "ip", uploadClientIP(r))
a.renderAuth(w, r, http.StatusNotFound, authPageData{Mode: "invite", Error: "This invite is invalid or expired."})
return
}
@@ -116,10 +124,11 @@ func (a *App) InvitePost(w http.ResponseWriter, r *http.Request) {
}
user, err := a.authService.AcceptInvite(token, r.FormValue("username"), r.FormValue("password"))
if err != nil {
a.logger.Warn("invite accept failed", "source", "auth", "severity", "warn", "code", 4405, "ip", uploadClientIP(r), "invite_email", invite.Email, "error", err.Error())
a.renderAuth(w, r, http.StatusBadRequest, authPageData{Mode: "invite", Token: token, Email: invite.Email, IsReset: invite.UserID != "", Error: err.Error()})
return
}
a.logger.Info("invite accepted", "source", "auth", "severity", "user_activity", "code", 2403, "user_id", user.ID)
a.logger.Info("invite accepted", "source", "auth", "severity", "user_activity", "code", 2403, "user_id", user.ID, "ip", uploadClientIP(r), "invite_email", invite.Email)
a.loginAndRedirect(w, r, user.Email, r.FormValue("password"), "/app")
}
@@ -176,6 +185,8 @@ func (a *App) DeleteUserToken(w http.ResponseWriter, r *http.Request) {
}
if err := a.authService.DeleteAPIToken(user.ID, r.PathValue("tokenID")); err != nil {
a.logger.Warn("api token delete failed", "source", "user_activity", "severity", "warn", "code", 4421, "user_id", user.ID, "error", err.Error())
} else {
a.logger.Info("api token deleted", "source", "user_activity", "severity", "user_activity", "code", 2421, "user_id", user.ID, "token_id", r.PathValue("tokenID"))
}
http.Redirect(w, r, "/account/settings", http.StatusSeeOther)
}
@@ -222,13 +233,16 @@ func (a *App) ChangePassword(w http.ResponseWriter, r *http.Request) {
return
}
if !services.VerifyPasswordHash(user.PasswordHash, r.FormValue("current_password")) {
a.logger.Warn("password change failed current password", "source", "user_activity", "severity", "warn", "code", 4422, "user_id", user.ID, "ip", uploadClientIP(r))
http.Redirect(w, r, "/account/settings", http.StatusSeeOther)
return
}
if err := a.authService.SetPassword(user.ID, r.FormValue("new_password")); err != nil {
a.logger.Warn("password change failed", "source", "user_activity", "severity", "warn", "code", 4423, "user_id", user.ID, "error", err.Error())
http.Redirect(w, r, "/account/settings", http.StatusSeeOther)
return
}
a.logger.Info("password changed", "source", "user_activity", "severity", "user_activity", "code", 2422, "user_id", user.ID, "ip", uploadClientIP(r))
http.Redirect(w, r, "/account/settings", http.StatusSeeOther)
}