From 3c11977c772683e5e90234ed1f0b1073513391c7 Mon Sep 17 00:00:00 2001 From: snvv133 <80438059+snvv133@users.noreply.github.com> Date: Sun, 19 Apr 2026 11:33:29 -0700 Subject: [PATCH] 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 //
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. --- web/controller/index.go | 5 ++--- web/controller/server.go | 8 +++++++- web/html/index.html | 24 ++++++++++++++++++++++-- web/web.go | 6 +++++- 4 files changed, 36 insertions(+), 7 deletions(-) diff --git a/web/controller/index.go b/web/controller/index.go index 5f9e1c2c..0aa82f68 100644 --- a/web/controller/index.go +++ b/web/controller/index.go @@ -74,11 +74,10 @@ func (a *IndexController) login(c *gin.Context) { user := a.userService.CheckUser(form.Username, form.Password, form.TwoFactorCode) timeStr := time.Now().Format("2006-01-02 15:04:05") 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)) - a.tgbot.UserLoginNotify(safeUser, safePass, getRemoteIp(c), timeStr, 0) + logger.Warningf("wrong username: \"%s\", IP: \"%s\"", safeUser, getRemoteIp(c)) + a.tgbot.UserLoginNotify(safeUser, "***", getRemoteIp(c), timeStr, 0) pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword")) return } diff --git a/web/controller/server.go b/web/controller/server.go index d32209e1..926f2cff 100644 --- a/web/controller/server.go +++ b/web/controller/server.go @@ -44,7 +44,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) { g.GET("/cpuHistory/:bucket", a.getCpuHistoryBucket) g.GET("/getXrayVersion", a.getXrayVersion) g.GET("/getConfigJson", a.getConfigJson) - g.GET("/getDb", a.getDb) + g.POST("/getDb", a.getDb) g.GET("/getNewUUID", a.getNewUUID) g.GET("/getNewX25519Cert", a.getNewX25519Cert) g.GET("/getNewmldsa65", a.getNewmldsa65) @@ -252,7 +252,13 @@ func (a *ServerController) getConfigJson(c *gin.Context) { } // 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) { + if c.GetHeader("X-Requested-With") != "XMLHttpRequest" { + c.AbortWithStatus(http.StatusForbidden) + return + } db, err := a.serverService.GetDb() if err != nil { jsonMsg(c, I18nWeb(c, "pages.index.getDatabaseError"), err) diff --git a/web/html/index.html b/web/html/index.html index bbbbb708..8b06da80 100644 --- a/web/html/index.html +++ b/web/html/index.html @@ -1067,8 +1067,28 @@ openBackup() { backupModal.show(); }, - exportDatabase() { - window.location = basePath + 'panel/api/server/getDb'; + async exportDatabase() { + 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() { const fileInput = document.createElement('input'); diff --git a/web/web.go b/web/web.go index 60934048..0e5e07ac 100644 --- a/web/web.go +++ b/web/web.go @@ -206,11 +206,15 @@ func (s *Server) initRouter() (*gin.Engine, error) { store := cookie.NewStore(secret) // Configure default session cookie options, including expiration (MaxAge) if sessionMaxAge, err := s.settingService.GetSessionMaxAge(); err == nil { + certFile, _ := s.settingService.GetCertFile() + keyFile, _ := s.settingService.GetKeyFile() + secureCookie := certFile != "" && keyFile != "" store.Options(sessions.Options{ Path: "/", MaxAge: sessionMaxAge * 60, // minutes -> seconds HttpOnly: true, - SameSite: http.SameSiteLaxMode, + Secure: secureCookie, + SameSite: http.SameSiteStrictMode, }) } engine.Use(sessions.Sessions("3x-ui", store))