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)
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
Loading…
Reference in a new issue