Further session protections and fixes

Use MaxAge instead of Expires
Verify if the cookie is not too old and not from the future
Verify if the user exists and unchanged
Refresh not sooner than 24h
Do not refresh temporary sessions
Delete cookies on logout
This commit is contained in:
0xCA 2023-12-28 16:20:13 +05:00
parent 91427427f2
commit bee5c54127
5 changed files with 156 additions and 7 deletions

View file

@ -94,10 +94,8 @@ func Login(db store.IStore) echo.HandlerFunc {
if userCorrect && passwordCorrect { if userCorrect && passwordCorrect {
ageMax := 0 ageMax := 0
expiration := time.Now().Add(24 * time.Hour)
if rememberMe { if rememberMe {
ageMax = 86400 * 7 ageMax = 86400 * 7
expiration = time.Now().Add(time.Duration(ageMax) * time.Second)
} }
cookiePath := util.BasePath cookiePath := util.BasePath
@ -116,8 +114,11 @@ func Login(db store.IStore) echo.HandlerFunc {
// set session_token // set session_token
tokenUID := xid.New().String() tokenUID := xid.New().String()
sess.Values["username"] = dbuser.Username sess.Values["username"] = dbuser.Username
sess.Values["user_hash"] = util.GetDBUserCRC32(dbuser)
sess.Values["admin"] = dbuser.Admin sess.Values["admin"] = dbuser.Admin
sess.Values["session_token"] = tokenUID sess.Values["session_token"] = tokenUID
sess.Values["max_age"] = ageMax
sess.Values["last_update"] = time.Now().UTC().Unix()
sess.Save(c.Request(), c.Response()) sess.Save(c.Request(), c.Response())
// set session_token in cookie // set session_token in cookie
@ -125,7 +126,7 @@ func Login(db store.IStore) echo.HandlerFunc {
cookie.Name = "session_token" cookie.Name = "session_token"
cookie.Path = cookiePath cookie.Path = cookiePath
cookie.Value = tokenUID cookie.Value = tokenUID
cookie.Expires = expiration cookie.MaxAge = ageMax
cookie.HttpOnly = true cookie.HttpOnly = true
cookie.SameSite = http.SameSiteLaxMode cookie.SameSite = http.SameSiteLaxMode
c.SetCookie(cookie) c.SetCookie(cookie)
@ -266,7 +267,7 @@ func UpdateUser(db store.IStore) echo.HandlerFunc {
log.Infof("Updated user information successfully") log.Infof("Updated user information successfully")
if previousUsername == currentUser(c) { if previousUsername == currentUser(c) {
setUser(c, user.Username, user.Admin) setUser(c, user.Username, user.Admin, util.GetDBUserCRC32(user))
} }
return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated user information successfully"}) return c.JSON(http.StatusOK, jsonHTTPResponse{true, "Updated user information successfully"})

View file

@ -25,6 +25,7 @@ func ValidSession(next echo.HandlerFunc) echo.HandlerFunc {
} }
} }
// ValidSession middleware must be used before RefreshSession
func RefreshSession(next echo.HandlerFunc) echo.HandlerFunc { func RefreshSession(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
doRefreshSession(c) doRefreshSession(c)
@ -50,28 +51,67 @@ func isValidSession(c echo.Context) bool {
if err != nil || sess.Values["session_token"] != cookie.Value { if err != nil || sess.Values["session_token"] != cookie.Value {
return false return false
} }
// Check time bounds
lastUpdate := getLastUpdate(sess)
maxAge := getMaxAge(sess)
// Temporary session is considered valid within 24h if browser is not closed before
// This value is not saved and is used as virtual expiration
if maxAge == 0 {
maxAge = 86400
}
expiration := lastUpdate + int64(maxAge)
now := time.Now().UTC().Unix()
if lastUpdate > now || expiration < now {
return false
}
// Check if user still exists and unchanged
username := fmt.Sprintf("%s", sess.Values["username"])
userHash := getUserHash(sess)
if uHash, ok := util.DBUsersToCRC32[username]; !ok || userHash != uHash {
return false
}
return true return true
} }
// Refreshes a "remember me" session when the user visits web pages (not API)
// Session must be valid before calling this function
// Refresh is performet at most once per 24h
func doRefreshSession(c echo.Context) { func doRefreshSession(c echo.Context) {
if util.DisableLogin { if util.DisableLogin {
return return
} }
sess, _ := session.Get("session", c) sess, _ := session.Get("session", c)
maxAge := getMaxAge(sess)
if maxAge <= 0 {
return
}
oldCookie, err := c.Cookie("session_token") oldCookie, err := c.Cookie("session_token")
if err != nil || sess.Values["session_token"] != oldCookie.Value { if err != nil || sess.Values["session_token"] != oldCookie.Value {
return return
} }
// Refresh no sooner than 24h
lastUpdate := getLastUpdate(sess)
expiration := lastUpdate + int64(getMaxAge(sess))
now := time.Now().UTC().Unix()
if expiration < now || now-lastUpdate < 86400 {
return
}
cookiePath := util.BasePath cookiePath := util.BasePath
if cookiePath == "" { if cookiePath == "" {
cookiePath = "/" cookiePath = "/"
} }
sess.Values["last_update"] = now
sess.Options = &sessions.Options{ sess.Options = &sessions.Options{
Path: cookiePath, Path: cookiePath,
MaxAge: sess.Options.MaxAge, MaxAge: maxAge,
HttpOnly: true, HttpOnly: true,
SameSite: http.SameSiteLaxMode, SameSite: http.SameSiteLaxMode,
} }
@ -81,12 +121,61 @@ func doRefreshSession(c echo.Context) {
cookie.Name = "session_token" cookie.Name = "session_token"
cookie.Path = cookiePath cookie.Path = cookiePath
cookie.Value = oldCookie.Value cookie.Value = oldCookie.Value
cookie.Expires = time.Now().Add(time.Duration(sess.Options.MaxAge) * time.Second) cookie.MaxAge = maxAge
cookie.HttpOnly = true cookie.HttpOnly = true
cookie.SameSite = http.SameSiteLaxMode cookie.SameSite = http.SameSiteLaxMode
c.SetCookie(cookie) c.SetCookie(cookie)
} }
// Get time in seconds this session is valid without updating
func getMaxAge(sess *sessions.Session) int {
if util.DisableLogin {
return 0
}
maxAge := sess.Values["max_age"]
switch typedMaxAge := maxAge.(type) {
case int:
return typedMaxAge
default:
return 0
}
}
// Get a timestamp in seconds of the last session update
func getLastUpdate(sess *sessions.Session) int64 {
if util.DisableLogin {
return 0
}
lastUpdate := sess.Values["last_update"]
switch typedLastUpdate := lastUpdate.(type) {
case int64:
return typedLastUpdate
default:
return 0
}
}
// Get CRC32 of a user at the moment of log in
// Any changes to user will result in logout of other (not updated) sessions
func getUserHash(sess *sessions.Session) uint32 {
if util.DisableLogin {
return 0
}
userHash := sess.Values["user_hash"]
switch typedUserHash := userHash.(type) {
case uint32:
return typedUserHash
default:
return 0
}
}
// currentUser to get username of logged in user // currentUser to get username of logged in user
func currentUser(c echo.Context) string { func currentUser(c echo.Context) string {
if util.DisableLogin { if util.DisableLogin {
@ -109,9 +198,10 @@ func isAdmin(c echo.Context) bool {
return admin == "true" return admin == "true"
} }
func setUser(c echo.Context, username string, admin bool) { func setUser(c echo.Context, username string, admin bool, userCRC32 uint32) {
sess, _ := session.Get("session", c) sess, _ := session.Get("session", c)
sess.Values["username"] = username sess.Values["username"] = username
sess.Values["user_hash"] = userCRC32
sess.Values["admin"] = admin sess.Values["admin"] = admin
sess.Save(c.Request(), c.Response()) sess.Save(c.Request(), c.Response())
} }
@ -120,7 +210,27 @@ func setUser(c echo.Context, username string, admin bool) {
func clearSession(c echo.Context) { func clearSession(c echo.Context) {
sess, _ := session.Get("session", c) sess, _ := session.Get("session", c)
sess.Values["username"] = "" sess.Values["username"] = ""
sess.Values["user_hash"] = 0
sess.Values["admin"] = false sess.Values["admin"] = false
sess.Values["session_token"] = "" sess.Values["session_token"] = ""
sess.Values["max_age"] = -1
sess.Options.MaxAge = -1
sess.Save(c.Request(), c.Response()) sess.Save(c.Request(), c.Response())
cookiePath := util.BasePath
if cookiePath == "" {
cookiePath = "/"
}
cookie, err := c.Cookie("session_token")
if err != nil {
cookie = new(http.Cookie)
}
cookie.Name = "session_token"
cookie.Path = cookiePath
cookie.MaxAge = -1
cookie.HttpOnly = true
cookie.SameSite = http.SameSiteLaxMode
c.SetCookie(cookie)
} }

View file

@ -163,6 +163,14 @@ func (o *JsonDB) Init() error {
} }
// init cache // init cache
for _, i := range results {
user := model.User{}
if err := json.Unmarshal([]byte(i), &user); err == nil {
util.DBUsersToCRC32[user.Username] = util.GetDBUserCRC32(user)
}
}
clients, err := o.GetClients(false) clients, err := o.GetClients(false)
if err != nil { if err != nil {
return nil return nil
@ -217,11 +225,13 @@ func (o *JsonDB) SaveUser(user model.User) error {
if err != nil { if err != nil {
return err return err
} }
util.DBUsersToCRC32[user.Username] = util.GetDBUserCRC32(user)
return output return output
} }
// DeleteUser func to remove user from the database // DeleteUser func to remove user from the database
func (o *JsonDB) DeleteUser(username string) error { func (o *JsonDB) DeleteUser(username string) error {
delete(util.DBUsersToCRC32, username)
return o.conn.Delete("users", username) return o.conn.Delete("users", username)
} }

View file

@ -5,3 +5,4 @@ import "sync"
var IPToSubnetRange = map[string]uint16{} var IPToSubnetRange = map[string]uint16{}
var TgUseridToClientID = map[int64][]string{} var TgUseridToClientID = map[int64][]string{}
var TgUseridToClientIDMutex sync.RWMutex var TgUseridToClientIDMutex sync.RWMutex
var DBUsersToCRC32 = map[string]uint32{}

View file

@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"hash/crc32"
"io" "io"
"io/fs" "io/fs"
"math/rand" "math/rand"
@ -827,3 +828,29 @@ func filterStringSlice(s []string, excludedStr string) []string {
} }
return filtered return filtered
} }
func GetDBUserCRC32(dbuser model.User) uint32 {
var isAdmin byte = 0
if dbuser.Admin {
isAdmin = 1
}
return crc32.ChecksumIEEE(ConcatMultipleSlices([]byte(dbuser.Username), []byte{isAdmin}, []byte(dbuser.PasswordHash), []byte(dbuser.Password)))
}
func ConcatMultipleSlices(slices ...[]byte) []byte {
var totalLen int
for _, s := range slices {
totalLen += len(s)
}
result := make([]byte, totalLen)
var i int
for _, s := range slices {
i += copy(result[i:], s)
}
return result
}