TASK-018: Fix session management vulnerabilities
- Store session tokens server-side on login (was: generated but not stored) - Add /api/v1/auth/logout endpoint for server-side session invalidation - Delete old sessions on login to prevent session fixation attacks - Add Cache-Control: no-store headers to all auth responses Security fixes: 1. Session identifiers now rotated on login (old sessions deleted) 2. Logout properly invalidates server-side session 3. Auth responses include anti-caching headers
This commit is contained in:
parent
20795e1ea8
commit
5ebf9925ed
|
|
@ -12,6 +12,7 @@ import (
|
|||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
|
@ -177,3 +178,63 @@ func authDelete(table, pkCol string, pkVal interface{}) error {
|
|||
_, err := authDB.Exec(query, pkVal)
|
||||
return err
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Session Management
|
||||
// ============================================================================
|
||||
|
||||
// SessionCreate creates a new session with token and expiry
|
||||
type Session struct {
|
||||
Token string `db:"token,pk"`
|
||||
DossierID string `db:"dossier_id"`
|
||||
CreatedAt int64 `db:"created_at"`
|
||||
ExpiresAt int64 `db:"expires_at"`
|
||||
}
|
||||
|
||||
func SessionCreate(token, dossierID string, maxAgeSeconds int) error {
|
||||
now := time.Now().Unix()
|
||||
s := &Session{
|
||||
Token: token,
|
||||
DossierID: dossierID,
|
||||
CreatedAt: now,
|
||||
ExpiresAt: now + int64(maxAgeSeconds),
|
||||
}
|
||||
return authSave("sessions", s)
|
||||
}
|
||||
|
||||
func SessionDelete(token string) error {
|
||||
return authDelete("sessions", "token", token)
|
||||
}
|
||||
|
||||
// FIX TASK-018: Delete all sessions for a dossier (session rotation on login)
|
||||
func SessionDeleteByDossier(dossierID string) error {
|
||||
_, err := authDB.Exec("DELETE FROM sessions WHERE dossier_id = ?", dossierID)
|
||||
return err
|
||||
}
|
||||
|
||||
func SessionGetDossier(token string) string {
|
||||
var s Session
|
||||
if err := authLoad("sessions", token, &s); err != nil {
|
||||
return ""
|
||||
}
|
||||
// Check expiry
|
||||
if time.Now().Unix() > s.ExpiresAt {
|
||||
SessionDelete(token)
|
||||
return ""
|
||||
}
|
||||
return s.DossierID
|
||||
}
|
||||
|
||||
func SessionCleanup() {
|
||||
authDB.Exec("DELETE FROM sessions WHERE expires_at < ?", time.Now().Unix())
|
||||
}
|
||||
|
||||
// DossierGetBySessionToken gets dossier by session token (for API auth)
|
||||
func DossierGetBySessionToken(token string) *Dossier {
|
||||
dossierID := SessionGetDossier(token)
|
||||
if dossierID == "" {
|
||||
return nil
|
||||
}
|
||||
d, _ := DossierGet(dossierID, dossierID)
|
||||
return d
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ func cors(w http.ResponseWriter, r *http.Request) bool {
|
|||
func initMobileAPI(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/auth/send", handleAPISend)
|
||||
mux.HandleFunc("/api/v1/auth/verify", handleAPIVerify)
|
||||
mux.HandleFunc("/api/v1/auth/logout", handleAPILogout) // FIX TASK-018: Server-side logout
|
||||
mux.HandleFunc("/api/v1/dashboard", handleAPIDashboard)
|
||||
mux.HandleFunc("/api/v1/prompts", handleAPIPrompts)
|
||||
mux.HandleFunc("/api/v1/prompts/respond", handleAPITrackerRespond)
|
||||
|
|
@ -98,6 +99,11 @@ func handleAPISend(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// FIX TASK-018: Prevent auth response caching
|
||||
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
|
||||
jsonOK(w, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
|
|
@ -124,9 +130,16 @@ func handleAPIVerify(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// Generate session token
|
||||
// Generate session token and store it
|
||||
token := generateSessionToken()
|
||||
// TODO: store session token when session management is implemented
|
||||
|
||||
// FIX TASK-018: Store session server-side with 30-day expiry
|
||||
// Session rotation: Delete any existing sessions for this dossier to prevent fixation
|
||||
lib.SessionDeleteByDossier(id) // Delete old sessions
|
||||
if err := lib.SessionCreate(token, id, 30*24*60*60); err != nil {
|
||||
jsonError(w, "Session creation failed", 500)
|
||||
return
|
||||
}
|
||||
|
||||
d, _ := lib.DossierGet("", id)
|
||||
needsOnboard := d == nil || d.Name == ""
|
||||
|
|
@ -134,9 +147,37 @@ func handleAPIVerify(w http.ResponseWriter, r *http.Request) {
|
|||
lib.SendSignal(fmt.Sprintf("Mobile login: %s", d.Name))
|
||||
}
|
||||
|
||||
// FIX TASK-018: Prevent session token caching in browser/proxy
|
||||
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
|
||||
jsonOK(w, APIVerifyResponse{Token: token, NeedsOnboard: needsOnboard})
|
||||
}
|
||||
|
||||
// FIX TASK-018: Server-side logout endpoint
|
||||
func handleAPILogout(w http.ResponseWriter, r *http.Request) {
|
||||
if cors(w, r) { return }
|
||||
if r.Method != "POST" {
|
||||
jsonError(w, "Method not allowed", 405)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete session from server-side store
|
||||
auth := r.Header.Get("Authorization")
|
||||
if strings.HasPrefix(auth, "Bearer ") {
|
||||
token := strings.TrimPrefix(auth, "Bearer ")
|
||||
lib.SessionDelete(token)
|
||||
}
|
||||
|
||||
// FIX TASK-018: Cache-Control headers to prevent session token caching
|
||||
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
|
||||
jsonOK(w, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
// --- Dashboard ---
|
||||
|
||||
type APIDossierEntry struct {
|
||||
|
|
|
|||
Loading…
Reference in New Issue