Files
3x-ui/internal/web/session/session.go
T
MHSanaei ce532e8df7 fix(web): start a fresh session at login
SetLoginUser added the user to whatever session the login request
carried, so its CSRF token outlived the login. Anyone can fetch an
anonymous session with its token, and a host on a sibling domain can
plant that cookie in the admin's browser. Clear the session before the
user is stored; the next page load or token fetch mints a new token.
2026-10-31 09:05:28 +00:00

197 lines
4.0 KiB
Go

package session
import (
"encoding/gob"
"net/http"
"time"
"github.com/mhsanaei/3x-ui/v3/internal/database"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
"github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
const (
loginUserKey = "LOGIN_USER"
loginEpochKey = "LOGIN_EPOCH"
apiAuthUserKey = "api_auth_user"
sessionCookieName = "3x-ui"
)
func init() {
gob.Register(model.User{})
}
func SetLoginUser(c *gin.Context, user *model.User) error {
if user == nil {
return nil
}
s := sessions.Default(c)
// Anyone can mint the anonymous session this request carried and plant it in a browser,
// so nothing in it, the CSRF token above all, outlives the privilege change.
s.Clear()
s.Set(loginUserKey, user.Id)
s.Set(loginEpochKey, user.LoginEpoch)
return s.Save()
}
func SetAPIAuthUser(c *gin.Context, user *model.User) {
if user == nil {
return
}
c.Set(apiAuthUserKey, user)
}
func GetLoginUser(c *gin.Context) *model.User {
if v, ok := c.Get(apiAuthUserKey); ok {
if u, ok2 := v.(*model.User); ok2 {
return u
}
}
s := sessions.Default(c)
obj := s.Get(loginUserKey)
if obj == nil {
return nil
}
userID, ok := sessionUserID(obj)
if !ok {
s.Delete(loginUserKey)
s.Delete(loginEpochKey)
if err := s.Save(); err != nil {
logger.Warning("session: failed to drop stale user payload:", err)
}
return nil
}
if legacyUserID, ok := legacySessionUserID(obj); ok {
s.Set(loginUserKey, legacyUserID)
if err := s.Save(); err != nil {
logger.Warning("session: failed to migrate legacy user payload:", err)
}
}
user, err := getUserByID(userID)
if err != nil {
logger.Warning("session: failed to load user:", err)
s.Delete(loginUserKey)
s.Delete(loginEpochKey)
if saveErr := s.Save(); saveErr != nil {
logger.Warning("session: failed to drop missing user:", saveErr)
}
return nil
}
if !sessionEpochMatches(s.Get(loginEpochKey), user.LoginEpoch) {
s.Delete(loginUserKey)
s.Delete(loginEpochKey)
if saveErr := s.Save(); saveErr != nil {
logger.Warning("session: failed to drop stale epoch:", saveErr)
}
return nil
}
return user
}
func sessionEpochMatches(cookieVal any, userEpoch int64) bool {
var got int64
switch v := cookieVal.(type) {
case nil:
case int64:
got = v
case int:
got = int64(v)
case int32:
got = int64(v)
case float64:
got = int64(v)
default:
return false
}
return got == userEpoch
}
func IsLogin(c *gin.Context) bool {
return GetLoginUser(c) != nil
}
func sessionUserID(obj any) (int, bool) {
switch v := obj.(type) {
case int:
return v, v > 0
case int64:
return int(v), v > 0
case int32:
return int(v), v > 0
case float64:
id := int(v)
return id, v == float64(id) && id > 0
case model.User:
return v.Id, v.Id > 0
case *model.User:
if v == nil {
return 0, false
}
return v.Id, v.Id > 0
default:
return 0, false
}
}
func legacySessionUserID(obj any) (int, bool) {
switch v := obj.(type) {
case model.User:
return v.Id, v.Id > 0
case *model.User:
if v == nil {
return 0, false
}
return v.Id, v.Id > 0
default:
return 0, false
}
}
func getUserByID(id int) (*model.User, error) {
db := database.GetDB()
if db == nil {
return nil, http.ErrServerClosed
}
user := &model.User{}
if err := db.Model(model.User{}).Where("id = ?", id).First(user).Error; err != nil {
return nil, err
}
return user, nil
}
func ClearSession(c *gin.Context) error {
s := sessions.Default(c)
s.Clear()
cookiePath := c.GetString("base_path")
if cookiePath == "" {
cookiePath = "/"
}
secure := c.Request.TLS != nil
s.Options(sessions.Options{
Path: cookiePath,
MaxAge: -1,
HttpOnly: true,
Secure: secure,
SameSite: http.SameSiteLaxMode,
})
if err := s.Save(); err != nil {
return err
}
if cookiePath != "/" {
http.SetCookie(c.Writer, &http.Cookie{
Name: sessionCookieName,
Value: "",
Path: "/",
MaxAge: -1,
Expires: time.Unix(0, 0),
HttpOnly: true,
Secure: secure,
SameSite: http.SameSiteLaxMode,
})
}
return nil
}