Disclaimer: found while debugging an Authelia issue with the help of Claude (Opus 5.5); findings verified with the repro below.
Downstream report: authelia/authelia#13292 (Authelia vendors a copy of this provider).
Version
github.com/fasthttp/session/v2 v2.5.9 (current master, 7082691)
Problem
providers/memory/provider.go:
func (p *Provider) getSessionKey(sessionID []byte) string {
return strconv.B2S(sessionID)
}
strconv.B2S (savsgio/gotils) is unsafe.String, a zero-copy view of the bytes. Save and Regenerate store
that string as a map key. When the session ID comes from the request (Session.getSessionID →
ctx.Request.Header.Cookie(name)), those bytes belong to fasthttp's reusable header/cookie buffer. The stored
key therefore mutates when the buffer is reused by a later request, and lookups for the correct ID miss.
Newly generated IDs are separate allocations, so the bug only affects sessions whose first Save uses a
client-supplied ID unknown to the store, for example a stale cookie after a server restart, since memory storage is
empty. In Authelia this breaks the WebAuthn passkey flow (challenge saved on GET, missing on the following POST).
Reproduction
package main
import (
"fmt"
"io"
"net"
"net/http"
"github.com/fasthttp/session/v2"
"github.com/fasthttp/session/v2/providers/memory"
"github.com/valyala/fasthttp"
)
func main() {
prov, _ := memory.New(memory.Config{})
cfg := session.NewDefaultConfig()
cfg.CookieName, cfg.Secure = "session", false
sess := session.New(cfg)
_ = sess.SetProvider(prov)
ln, _ := net.Listen("tcp", "127.0.0.1:0")
go fasthttp.Serve(ln, func(ctx *fasthttp.RequestCtx) {
store, _ := sess.Get(ctx)
if string(ctx.Path()) == "/set" {
store.Set("k", "v")
_ = sess.Save(ctx, store)
return
}
if store.Get("k") == nil {
fmt.Fprint(ctx, "MISSING")
} else {
fmt.Fprint(ctx, "PRESENT")
}
})
c := &http.Client{Transport: &http.Transport{MaxConnsPerHost: 1}} // single keep-alive conn
req := func(path, cookie string) string {
r, _ := http.NewRequest("GET", "http://"+ln.Addr().String()+path, nil)
r.Header.Set("Cookie", cookie)
resp, err := c.Do(r)
if err != nil {
return err.Error()
}
defer resp.Body.Close()
b, _ := io.ReadAll(resp.Body)
return string(b)
}
// Client-supplied ID unknown to the store (e.g. stale cookie after restart).
req("/set", "session=STALEbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
// Same session cookie, different position in the Cookie header.
fmt.Println("result:", req("/get", "x=1; session=STALEbbbbbbbbbbbbbbbbbbbbbbbbbbbb"))
}
Output: result: MISSING. Dumping the map keys afterwards shows the stored key changed from STALEbbbb… to
1TALEbbbb…: the first byte was overwritten by the other cookie's value in the reused buffer.
Expected
result: PRESENT, which is what you get with getSessionKey returning string(sessionID).
Suggested fix
Copy the ID when it becomes a stored key:
func (p *Provider) getSessionKey(sessionID []byte) string {
return string(sessionID)
}
Alternatively, keep B2S for Get/Destroy, since those lookups don't retain the key, and copy only in
Save/Regenerate. It may also be worth copying the ID in Session.Get itself (store.sessionID), since any provider
that retains the slice has the same exposure. The Redis provider is unaffected because it builds its key
in a buffer and String() copies it.
Disclaimer: found while debugging an Authelia issue with the help of Claude (Opus 5.5); findings verified with the repro below.
Downstream report: authelia/authelia#13292 (Authelia vendors a copy of this provider).
Version
github.com/fasthttp/session/v2v2.5.9 (current master, 7082691)Problem
providers/memory/provider.go:strconv.B2S(savsgio/gotils) isunsafe.String, a zero-copy view of the bytes.SaveandRegeneratestorethat string as a map key. When the session ID comes from the request (
Session.getSessionID→ctx.Request.Header.Cookie(name)), those bytes belong to fasthttp's reusable header/cookie buffer. The storedkey therefore mutates when the buffer is reused by a later request, and lookups for the correct ID miss.
Newly generated IDs are separate allocations, so the bug only affects sessions whose first
Saveuses aclient-supplied ID unknown to the store, for example a stale cookie after a server restart, since memory storage is
empty. In Authelia this breaks the WebAuthn passkey flow (challenge saved on GET, missing on the following POST).
Reproduction
Output:
result: MISSING. Dumping the map keys afterwards shows the stored key changed fromSTALEbbbb…to1TALEbbbb…: the first byte was overwritten by the other cookie's value in the reused buffer.Expected
result: PRESENT, which is what you get withgetSessionKeyreturningstring(sessionID).Suggested fix
Copy the ID when it becomes a stored key:
Alternatively, keep
B2SforGet/Destroy, since those lookups don't retain the key, and copy only inSave/Regenerate. It may also be worth copying the ID inSession.Getitself (store.sessionID), since any providerthat retains the slice has the same exposure. The Redis provider is unaffected because it builds its key
in a buffer and
String()copies it.