R-35: an ended session cannot return after a failed save (password fingerprint in the file; a failed revoking save removes it) — security review
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -195,3 +195,54 @@ func TestR35_CorruptFileStartsEmpty(t *testing.T) {
|
||||
t.Fatal("after a corrupt file, a new session must persist again")
|
||||
}
|
||||
}
|
||||
|
||||
// Security review 2026-10-08: a password change revokes on disk even when its own save never happened. Here the
|
||||
// password changes in settings.json WITHOUT invalidateAllSessions (the shape of a failed save that also could not
|
||||
// remove the file); the restart drops every row written under the old password.
|
||||
// RED-PROOF: skip the CredentialFP comparison in loadSessions → the old cookie signs in after the restart → FAILS.
|
||||
func TestR35_PasswordChangeRevokesEvenIfSaveFailed(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
s1 := newR35Server(t, dir)
|
||||
tok := s1.createSession()
|
||||
s1.Close()
|
||||
s2 := newR35Server(t, dir)
|
||||
if !s2.isValidSession(tok) {
|
||||
t.Fatal("setup: the session must survive one restart first (positive control)")
|
||||
}
|
||||
if err := s2.settings.SetPasswordHash("$2a$10$abcdefghijklmnopqrstuvABCDEFGHIJKLMNOPQRSTUVWXYZ01234"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s2.Close()
|
||||
s3 := newR35Server(t, dir)
|
||||
if s3.isValidSession(tok) {
|
||||
t.Fatal("a session written under the old password signs in after the password changed")
|
||||
}
|
||||
}
|
||||
|
||||
// Security review 2026-10-08: a logout whose save fails removes the file, so the signed-out cookie cannot return.
|
||||
// The save is made to fail by a DIRECTORY at the temp path (OpenFile on a directory fails).
|
||||
// RED-PROOF: make saveSessionsRevokingLocked only call saveSessionsLocked → the stale file keeps the row → FAILS.
|
||||
func TestR35_FailedLogoutSaveRemovesFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
s1 := newR35Server(t, dir)
|
||||
tok := s1.createSession()
|
||||
s1.Close()
|
||||
s2 := newR35Server(t, dir)
|
||||
if !s2.isValidSession(tok) {
|
||||
t.Fatal("setup: the session must survive one restart first (positive control)")
|
||||
}
|
||||
if err := os.Mkdir(filepath.Join(dir, sessionsFileName+".tmp"), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/logout", nil)
|
||||
req.AddCookie(&http.Cookie{Name: sessionCookieName, Value: tok})
|
||||
s2.handleLogout(httptest.NewRecorder(), req)
|
||||
s2.Close()
|
||||
if err := os.Remove(filepath.Join(dir, sessionsFileName+".tmp")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s3 := newR35Server(t, dir)
|
||||
if s3.isValidSession(tok) {
|
||||
t.Fatal("logout with a failed save: the signed-out cookie signs in again after a restart")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user