feat(auth): block panel with default admin/admin credentials and guide credential change

checkLogin middleware now detects default admin/admin credentials and
redirects every panel route to /panel/settings until they are changed.
The settings page auto-opens the Authentication tab, shows a
non-dismissible error banner, and lists 'Default credentials' first in
the security checklist. Login response includes mustChangeCredentials
so the login page can redirect directly. Logout is now POST-only.
Password must be at least 10 characters and cannot be admin/admin.
This commit is contained in:
farhadh
2026-05-11 21:09:48 +02:00
parent ce88b0b432
commit 56ce6073ce
8 changed files with 228 additions and 42 deletions
+34 -1
View File
@@ -4,8 +4,10 @@ package controller
import (
"net/http"
"strings"
"github.com/mhsanaei/3x-ui/v3/logger"
"github.com/mhsanaei/3x-ui/v3/util/crypto"
"github.com/mhsanaei/3x-ui/v3/web/locale"
"github.com/mhsanaei/3x-ui/v3/web/session"
@@ -17,7 +19,8 @@ type BaseController struct{}
// checkLogin is a middleware that verifies user authentication and handles unauthorized access.
func (a *BaseController) checkLogin(c *gin.Context) {
if !session.IsLogin(c) {
user := session.GetLoginUser(c)
if user == nil {
if isAjax(c) {
pureJsonMsg(c, http.StatusUnauthorized, false, I18nWeb(c, "pages.login.loginAgain"))
} else {
@@ -25,11 +28,41 @@ func (a *BaseController) checkLogin(c *gin.Context) {
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
}
c.Abort()
return
}
if isDefaultAdminCredential(user.Username, user.Password) && !credentialChangeRouteAllowed(c) {
if isAjax(c) {
pureJsonMsg(c, http.StatusForbidden, false, "Change the default admin credentials before continuing.")
} else {
c.Header("Cache-Control", "no-store")
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path")+"panel/settings")
}
c.Abort()
} else {
c.Next()
}
}
func isDefaultAdminCredential(username string, hashedPassword string) bool {
return username == "admin" && crypto.CheckPasswordHash(hashedPassword, "admin")
}
func credentialChangeRouteAllowed(c *gin.Context) bool {
basePath := c.GetString("base_path")
path := c.Request.URL.Path
allowedPrefixes := []string{
basePath + "panel/settings",
basePath + "panel/setting/",
basePath + "panel/csrf-token",
}
for _, prefix := range allowedPrefixes {
if strings.HasPrefix(path, prefix) {
return true
}
}
return false
}
// I18nWeb retrieves an internationalized message for the web interface based on the current locale.
func I18nWeb(c *gin.Context, name string, params ...string) string {
anyfunc, funcExists := c.Get("I18n")