mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
security: fix password log leak, getDb CSRF, cookie hardening
1. web/controller/index.go Stop logging the submitted plaintext password on failed login. Replace it with "***" in the Telegram notification too. 2. web/controller/server.go + web/html/index.html Convert /panel/api/server/getDb from GET to POST and require an X-Requested-With header. Prevents <img>/<a>/<form> CSRF that would otherwise let an attacker steal the SQLite DB by tricking a logged-in admin into loading a single URL. 3. web/web.go Set Secure=true on the session cookie when TLS cert/key are configured, and tighten SameSite from Lax to Strict for the panel session.
This commit is contained in:
parent
52fdf5d429
commit
3c11977c77
4 changed files with 36 additions and 7 deletions
|
|
@ -74,11 +74,10 @@ func (a *IndexController) login(c *gin.Context) {
|
||||||
user := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode)
|
user := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode)
|
||||||
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
||||||
safeUser := template.HTMLEscapeString(form.Username)
|
safeUser := template.HTMLEscapeString(form.Username)
|
||||||
safePass := template.HTMLEscapeString(form.Password)
|
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
logger.Warningf("wrong username: \"%s\", password: \"%s\", IP: \"%s\"", safeUser, safePass, getRemoteIp(c))
|
logger.Warningf("wrong username: \"%s\", IP: \"%s\"", safeUser, getRemoteIp(c))
|
||||||
a.tgbot.UserLoginNotify(safeUser, safePass, getRemoteIp(c), timeStr, 0)
|
a.tgbot.UserLoginNotify(safeUser, "***", getRemoteIp(c), timeStr, 0)
|
||||||
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||||
g.GET("/cpuHistory/:bucket", a.getCpuHistoryBucket)
|
g.GET("/cpuHistory/:bucket", a.getCpuHistoryBucket)
|
||||||
g.GET("/getXrayVersion", a.getXrayVersion)
|
g.GET("/getXrayVersion", a.getXrayVersion)
|
||||||
g.GET("/getConfigJson", a.getConfigJson)
|
g.GET("/getConfigJson", a.getConfigJson)
|
||||||
g.GET("/getDb", a.getDb)
|
g.POST("/getDb", a.getDb)
|
||||||
g.GET("/getNewUUID", a.getNewUUID)
|
g.GET("/getNewUUID", a.getNewUUID)
|
||||||
g.GET("/getNewX25519Cert", a.getNewX25519Cert)
|
g.GET("/getNewX25519Cert", a.getNewX25519Cert)
|
||||||
g.GET("/getNewmldsa65", a.getNewmldsa65)
|
g.GET("/getNewmldsa65", a.getNewmldsa65)
|
||||||
|
|
@ -252,7 +252,13 @@ func (a *ServerController) getConfigJson(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// getDb downloads the database file.
|
// getDb downloads the database file.
|
||||||
|
// CSRF mitigation: requires X-Requested-With header (cannot be sent cross-origin
|
||||||
|
// from a simple form/img/anchor without a preflight, which SameSite=Strict blocks).
|
||||||
func (a *ServerController) getDb(c *gin.Context) {
|
func (a *ServerController) getDb(c *gin.Context) {
|
||||||
|
if c.GetHeader("X-Requested-With") != "XMLHttpRequest" {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
db, err := a.serverService.GetDb()
|
db, err := a.serverService.GetDb()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18nWeb(c, "pages.index.getDatabaseError"), err)
|
jsonMsg(c, I18nWeb(c, "pages.index.getDatabaseError"), err)
|
||||||
|
|
|
||||||
|
|
@ -1067,8 +1067,28 @@
|
||||||
openBackup() {
|
openBackup() {
|
||||||
backupModal.show();
|
backupModal.show();
|
||||||
},
|
},
|
||||||
exportDatabase() {
|
async exportDatabase() {
|
||||||
window.location = basePath + 'panel/api/server/getDb';
|
try {
|
||||||
|
const resp = await fetch(basePath + 'panel/api/server/getDb', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||||
|
credentials: 'same-origin',
|
||||||
|
});
|
||||||
|
if (!resp.ok) {
|
||||||
|
throw new Error('HTTP ' + resp.status);
|
||||||
|
}
|
||||||
|
const blob = await resp.blob();
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = 'x-ui.db';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('export db failed', e);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
importDatabase() {
|
importDatabase() {
|
||||||
const fileInput = document.createElement('input');
|
const fileInput = document.createElement('input');
|
||||||
|
|
|
||||||
|
|
@ -206,11 +206,15 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
store := cookie.NewStore(secret)
|
store := cookie.NewStore(secret)
|
||||||
// Configure default session cookie options, including expiration (MaxAge)
|
// Configure default session cookie options, including expiration (MaxAge)
|
||||||
if sessionMaxAge, err := s.settingService.GetSessionMaxAge(); err == nil {
|
if sessionMaxAge, err := s.settingService.GetSessionMaxAge(); err == nil {
|
||||||
|
certFile, _ := s.settingService.GetCertFile()
|
||||||
|
keyFile, _ := s.settingService.GetKeyFile()
|
||||||
|
secureCookie := certFile != "" && keyFile != ""
|
||||||
store.Options(sessions.Options{
|
store.Options(sessions.Options{
|
||||||
Path: "/",
|
Path: "/",
|
||||||
MaxAge: sessionMaxAge * 60, // minutes -> seconds
|
MaxAge: sessionMaxAge * 60, // minutes -> seconds
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
SameSite: http.SameSiteLaxMode,
|
Secure: secureCookie,
|
||||||
|
SameSite: http.SameSiteStrictMode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
engine.Use(sessions.Sessions("3x-ui", store))
|
engine.Use(sessions.Sessions("3x-ui", store))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue