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:
snvv133 2026-04-19 11:33:29 -07:00
parent 52fdf5d429
commit 3c11977c77
4 changed files with 36 additions and 7 deletions

View file

@ -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
}

View file

@ -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)

View file

@ -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');

View file

@ -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))