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

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 {
return func(c echo.Context) error {
doRefreshSession(c)
@ -50,28 +51,67 @@ func isValidSession(c echo.Context) bool {
if err != nil || sess.Values["session_token"] != cookie.Value {
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
}
// 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) {
if util.DisableLogin {
return
}
sess, _ := session.Get("session", c)
maxAge := getMaxAge(sess)
if maxAge <= 0 {
return
}
oldCookie, err := c.Cookie("session_token")
if err != nil || sess.Values["session_token"] != oldCookie.Value {
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
if cookiePath == "" {
cookiePath = "/"
}
sess.Values["last_update"] = now
sess.Options = &sessions.Options{
Path: cookiePath,
MaxAge: sess.Options.MaxAge,
MaxAge: maxAge,
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
}
@ -81,12 +121,61 @@ func doRefreshSession(c echo.Context) {
cookie.Name = "session_token"
cookie.Path = cookiePath
cookie.Value = oldCookie.Value
cookie.Expires = time.Now().Add(time.Duration(sess.Options.MaxAge) * time.Second)
cookie.MaxAge = maxAge
cookie.HttpOnly = true
cookie.SameSite = http.SameSiteLaxMode
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
func currentUser(c echo.Context) string {
if util.DisableLogin {
@ -109,9 +198,10 @@ func isAdmin(c echo.Context) bool {
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.Values["username"] = username
sess.Values["user_hash"] = userCRC32
sess.Values["admin"] = admin
sess.Save(c.Request(), c.Response())
}
@ -120,7 +210,27 @@ func setUser(c echo.Context, username string, admin bool) {
func clearSession(c echo.Context) {
sess, _ := session.Get("session", c)
sess.Values["username"] = ""
sess.Values["user_hash"] = 0
sess.Values["admin"] = false
sess.Values["session_token"] = ""
sess.Values["max_age"] = -1
sess.Options.MaxAge = -1
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
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)
if err != nil {
return nil
@ -217,11 +225,13 @@ func (o *JsonDB) SaveUser(user model.User) error {
if err != nil {
return err
}
util.DBUsersToCRC32[user.Username] = util.GetDBUserCRC32(user)
return output
}
// DeleteUser func to remove user from the database
func (o *JsonDB) DeleteUser(username string) error {
delete(util.DBUsersToCRC32, username)
return o.conn.Delete("users", username)
}

View file

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

View file

@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
"hash/crc32"
"io"
"io/fs"
"math/rand"
@ -827,3 +828,29 @@ func filterStringSlice(s []string, excludedStr string) []string {
}
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
}