security: rotate session ID on login to prevent session fixation
The server-side session store (gorilla/sessions backed by DB) reused the same session ID across login: session.Clear() only zeroed the values map but left session.ID unchanged. An attacker who planted a known session ID before authentication retained access after the victim logged in. Fix with a two-phase save: 1. Delete the old session from the DB (MaxAge=-1 save), expiring the cookie. 2. Reset the underlying gorilla Session.ID to "" so the store generates a fresh ID, then save the authenticated session with original cookie options (Secure, Path, MaxAge) preserved via a duck-typed interface assertion. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b0b79efceb
commit
90f07a215c
1 changed files with 38 additions and 3 deletions
|
|
@ -26,8 +26,9 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
ginsessions "github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
gorillasessions "github.com/gorilla/sessions"
|
||||
|
||||
"git.happydns.org/happyDomain/model"
|
||||
)
|
||||
|
|
@ -43,15 +44,49 @@ func AuthRequired() gin.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
// gorillasessionExposer is satisfied by the concrete gin-contrib/sessions
|
||||
// type, which wraps a *gorilla/sessions.Session and exposes it via Session().
|
||||
// Using a duck-typed local interface avoids importing gin-contrib internals.
|
||||
type gorillasessionExposer interface {
|
||||
Session() *gorillasessions.Session
|
||||
}
|
||||
|
||||
func SessionLoginOK(c *gin.Context, user happydns.UserInfo) error {
|
||||
session := sessions.Default(c)
|
||||
session := ginsessions.Default(c)
|
||||
|
||||
// Phase 1: invalidate the pre-login session to prevent session fixation.
|
||||
// Preserve the original session options (Secure flag, Path, MaxAge) so
|
||||
// we can restore them on the new session.
|
||||
// Setting MaxAge=-1 causes the store to delete the server-side record and
|
||||
// send an expired cookie on Save().
|
||||
var origOptions *gorillasessions.Options
|
||||
if gs, ok := session.(gorillasessionExposer); ok {
|
||||
if gs.Session().Options != nil {
|
||||
opts := *gs.Session().Options // copy by value
|
||||
origOptions = &opts
|
||||
}
|
||||
}
|
||||
|
||||
session.Clear()
|
||||
session.Options(ginsessions.Options{MaxAge: -1})
|
||||
session.Save()
|
||||
|
||||
// Phase 2: create a genuinely new session with a fresh ID.
|
||||
// Reset the gorilla session's ID so the store generates a new one,
|
||||
// then restore the original cookie options.
|
||||
if gs, ok := session.(gorillasessionExposer); ok {
|
||||
gs.Session().ID = ""
|
||||
if origOptions != nil {
|
||||
origOptions.MaxAge = 86400 * 30 // restore positive MaxAge
|
||||
gs.Session().Options = origOptions
|
||||
}
|
||||
}
|
||||
|
||||
session.Set("iduser", user.GetUserId())
|
||||
err := session.Save()
|
||||
if err != nil {
|
||||
return happydns.InternalError{
|
||||
Err: fmt.Errorf("failed to save save user session: %s", err),
|
||||
Err: fmt.Errorf("failed to save user session: %s", err),
|
||||
UserMessage: "Invalid username or password.",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue