mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-07 12:54:33 +00:00
Implement CSRF protection and security hardening across the application (#4179)
* Implement CSRF protection and security hardening across the application - Added CSRF token handling in axios requests and HTML templates. - Introduced CSRF middleware to validate tokens for unsafe HTTP methods. - Implemented login limiter to prevent brute-force attacks. - Enhanced security headers in middleware for improved response security. - Updated login notification to include safe metadata without passwords. - Added tests for CSRF middleware and login limiter functionality. * fix
This commit is contained in:
committed by
GitHub
parent
a1b2382877
commit
10ebc6cbdc
@@ -1,12 +1,12 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/logger"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/middleware"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||
|
||||
@@ -41,8 +41,8 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
|
||||
g.GET("/", a.index)
|
||||
g.GET("/logout", a.logout)
|
||||
|
||||
g.POST("/login", a.login)
|
||||
g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
|
||||
g.POST("/login", middleware.CSRFMiddleware(), a.login)
|
||||
g.POST("/getTwoFactorEnable", middleware.CSRFMiddleware(), a.getTwoFactorEnable)
|
||||
}
|
||||
|
||||
// index handles the root route, redirecting logged-in users to the panel or showing the login page.
|
||||
@@ -71,28 +71,51 @@ func (a *IndexController) login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
user, checkErr := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode)
|
||||
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
||||
remoteIP := getRemoteIp(c)
|
||||
safeUser := template.HTMLEscapeString(form.Username)
|
||||
safePass := template.HTMLEscapeString(form.Password)
|
||||
|
||||
if user == nil {
|
||||
logger.Warningf("wrong username: \"%s\", password: \"%s\", IP: \"%s\"", safeUser, safePass, getRemoteIp(c))
|
||||
|
||||
notifyPass := safePass
|
||||
|
||||
if checkErr != nil && checkErr.Error() == "invalid 2fa code" {
|
||||
translatedError := a.tgbot.I18nBot("tgbot.messages.2faFailed")
|
||||
notifyPass = fmt.Sprintf("*** (%s)", translatedError)
|
||||
}
|
||||
|
||||
a.tgbot.UserLoginNotify(safeUser, notifyPass, getRemoteIp(c), timeStr, 0)
|
||||
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
||||
if blockedUntil, ok := defaultLoginLimiter.allow(remoteIP, form.Username); !ok {
|
||||
reason := "too many failed attempts"
|
||||
logger.Warningf("failed login: username=%q, IP=%q, reason=%q, blocked_until=%s", safeUser, remoteIP, reason, blockedUntil.Format(time.RFC3339))
|
||||
a.tgbot.UserLoginNotify(service.LoginAttempt{
|
||||
Username: safeUser,
|
||||
IP: remoteIP,
|
||||
Time: timeStr,
|
||||
Status: service.LoginFail,
|
||||
Reason: reason,
|
||||
})
|
||||
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
||||
return
|
||||
}
|
||||
|
||||
logger.Infof("%s logged in successfully, Ip Address: %s\n", safeUser, getRemoteIp(c))
|
||||
a.tgbot.UserLoginNotify(safeUser, ``, getRemoteIp(c), timeStr, 1)
|
||||
user, checkErr := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode)
|
||||
|
||||
if user == nil {
|
||||
reason := loginFailureReason(checkErr)
|
||||
if blockedUntil, blocked := defaultLoginLimiter.registerFailure(remoteIP, form.Username); blocked {
|
||||
logger.Warningf("failed login: username=%q, IP=%q, reason=%q, blocked_until=%s", safeUser, remoteIP, reason, blockedUntil.Format(time.RFC3339))
|
||||
} else {
|
||||
logger.Warningf("failed login: username=%q, IP=%q, reason=%q", safeUser, remoteIP, reason)
|
||||
}
|
||||
a.tgbot.UserLoginNotify(service.LoginAttempt{
|
||||
Username: safeUser,
|
||||
IP: remoteIP,
|
||||
Time: timeStr,
|
||||
Status: service.LoginFail,
|
||||
Reason: reason,
|
||||
})
|
||||
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
||||
return
|
||||
}
|
||||
|
||||
defaultLoginLimiter.registerSuccess(remoteIP, form.Username)
|
||||
logger.Infof("%s logged in successfully, Ip Address: %s\n", safeUser, remoteIP)
|
||||
a.tgbot.UserLoginNotify(service.LoginAttempt{
|
||||
Username: safeUser,
|
||||
IP: remoteIP,
|
||||
Time: timeStr,
|
||||
Status: service.LoginSuccess,
|
||||
})
|
||||
|
||||
if err := session.SetLoginUser(c, user); err != nil {
|
||||
logger.Warning("Unable to save session:", err)
|
||||
@@ -103,6 +126,13 @@ func (a *IndexController) login(c *gin.Context) {
|
||||
jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), nil)
|
||||
}
|
||||
|
||||
func loginFailureReason(err error) string {
|
||||
if err != nil && err.Error() == "invalid 2fa code" {
|
||||
return "invalid 2FA code"
|
||||
}
|
||||
return "invalid credentials"
|
||||
}
|
||||
|
||||
// logout handles user logout by clearing the session and redirecting to the login page.
|
||||
func (a *IndexController) logout(c *gin.Context) {
|
||||
user := session.GetLoginUser(c)
|
||||
|
||||
Reference in New Issue
Block a user