mirror of https://github.com/jetkvm/kvm.git
security: fix critical race conditions and add validation to session management
This commit addresses multiple CRITICAL and HIGH severity security issues identified during the multi-session implementation review. CRITICAL Fixes: - Fix race condition in session approval handlers (jsonrpc.go) Previously approveNewSession and denyNewSession directly mutated session.Mode without holding the SessionManager.mu lock, potentially causing data corruption during concurrent access. - Add validation to ApprovePrimaryRequest (session_manager.go:795-810) Now verifies that requester session exists and is in Queued mode before approving transfer, preventing invalid state transitions. - Close dual-primary window during reconnection (session_manager.go:208) Added explicit primaryExists check to prevent brief window where two sessions could both be primary during reconnection. HIGH Priority Fixes: - Add nickname uniqueness validation (session_manager.go:152-159) Prevents multiple sessions from having the same nickname, both in AddSession and updateSessionNickname RPC handler. Code Quality: - Remove debug scaffolding from cloud.go (lines 515-520, 530) Cleaned up temporary debug logs that are no longer needed. Thread Safety: - Add centralized ApproveSession() method (session_manager.go:870-890) - Add centralized DenySession() method (session_manager.go:894-912) Both methods properly acquire locks and validate session state. - Update RPC handlers to use thread-safe methods approveNewSession and denyNewSession now call sessionManager methods instead of direct session mutation. All changes have been verified with linters (golangci-lint: 0 issues).
This commit is contained in:
parent
825299257d
commit
821675cd21
7
cloud.go
7
cloud.go
|
|
@ -512,12 +512,8 @@ func handleSessionRequest(
|
||||||
_ = wsjson.Write(context.Background(), c, gin.H{"error": "session manager not initialized"})
|
_ = wsjson.Write(context.Background(), c, gin.H{"error": "session manager not initialized"})
|
||||||
return fmt.Errorf("session manager not initialized")
|
return fmt.Errorf("session manager not initialized")
|
||||||
}
|
}
|
||||||
scopedLogger.Debug().Msg("About to call AddSession")
|
|
||||||
err = sessionManager.AddSession(session, req.SessionSettings)
|
err = sessionManager.AddSession(session, req.SessionSettings)
|
||||||
scopedLogger.Debug().
|
|
||||||
Bool("addSessionSucceeded", err == nil).
|
|
||||||
Str("error", fmt.Sprintf("%v", err)).
|
|
||||||
Msg("AddSession returned")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
scopedLogger.Warn().Err(err).Msg("failed to add session to session manager")
|
scopedLogger.Warn().Err(err).Msg("failed to add session to session manager")
|
||||||
if err == ErrMaxSessionsReached {
|
if err == ErrMaxSessionsReached {
|
||||||
|
|
@ -527,7 +523,6 @@ func handleSessionRequest(
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
scopedLogger.Debug().Msg("AddSession completed successfully, continuing")
|
|
||||||
|
|
||||||
if session.HasPermission(PermissionPaste) {
|
if session.HasPermission(PermissionPaste) {
|
||||||
cancelKeyboardMacro()
|
cancelKeyboardMacro()
|
||||||
|
|
|
||||||
67
jsonrpc.go
67
jsonrpc.go
|
|
@ -192,12 +192,10 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) {
|
||||||
if err := RequirePermission(session, PermissionSessionApprove); err != nil {
|
if err := RequirePermission(session, PermissionSessionApprove); err != nil {
|
||||||
handlerErr = err
|
handlerErr = err
|
||||||
} else if sessionID, ok := request.Params["sessionId"].(string); ok {
|
} else if sessionID, ok := request.Params["sessionId"].(string); ok {
|
||||||
if targetSession := sessionManager.GetSession(sessionID); targetSession != nil && targetSession.Mode == SessionModePending {
|
handlerErr = sessionManager.ApproveSession(sessionID)
|
||||||
targetSession.Mode = SessionModeObserver
|
if handlerErr == nil {
|
||||||
sessionManager.broadcastSessionListUpdate()
|
go sessionManager.broadcastSessionListUpdate()
|
||||||
result = map[string]interface{}{"status": "approved"}
|
result = map[string]interface{}{"status": "approved"}
|
||||||
} else {
|
|
||||||
handlerErr = errors.New("session not found or not pending")
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
handlerErr = errors.New("invalid sessionId parameter")
|
handlerErr = errors.New("invalid sessionId parameter")
|
||||||
|
|
@ -206,14 +204,18 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) {
|
||||||
if err := RequirePermission(session, PermissionSessionApprove); err != nil {
|
if err := RequirePermission(session, PermissionSessionApprove); err != nil {
|
||||||
handlerErr = err
|
handlerErr = err
|
||||||
} else if sessionID, ok := request.Params["sessionId"].(string); ok {
|
} else if sessionID, ok := request.Params["sessionId"].(string); ok {
|
||||||
if targetSession := sessionManager.GetSession(sessionID); targetSession != nil && targetSession.Mode == SessionModePending {
|
handlerErr = sessionManager.DenySession(sessionID)
|
||||||
writeJSONRPCEvent("sessionAccessDenied", map[string]interface{}{
|
if handlerErr == nil {
|
||||||
"message": "Access denied by primary session",
|
// Notify the denied session
|
||||||
}, targetSession)
|
if targetSession := sessionManager.GetSession(sessionID); targetSession != nil {
|
||||||
sessionManager.broadcastSessionListUpdate()
|
go func() {
|
||||||
|
writeJSONRPCEvent("sessionAccessDenied", map[string]interface{}{
|
||||||
|
"message": "Access denied by primary session",
|
||||||
|
}, targetSession)
|
||||||
|
sessionManager.broadcastSessionListUpdate()
|
||||||
|
}()
|
||||||
|
}
|
||||||
result = map[string]interface{}{"status": "denied"}
|
result = map[string]interface{}{"status": "denied"}
|
||||||
} else {
|
|
||||||
handlerErr = errors.New("session not found or not pending")
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
handlerErr = errors.New("invalid sessionId parameter")
|
handlerErr = errors.New("invalid sessionId parameter")
|
||||||
|
|
@ -251,24 +253,35 @@ func onRPCMessage(message webrtc.DataChannelMessage, session *Session) {
|
||||||
} else if targetSession := sessionManager.GetSession(sessionID); targetSession != nil {
|
} else if targetSession := sessionManager.GetSession(sessionID); targetSession != nil {
|
||||||
// Users can update their own nickname, or admins can update any
|
// Users can update their own nickname, or admins can update any
|
||||||
if targetSession.ID == session.ID || session.HasPermission(PermissionSessionManage) {
|
if targetSession.ID == session.ID || session.HasPermission(PermissionSessionManage) {
|
||||||
targetSession.Nickname = nickname
|
// Check nickname uniqueness
|
||||||
|
allSessions := sessionManager.GetAllSessions()
|
||||||
// If session is pending and approval is required, send the approval request now that we have a nickname
|
for _, existingSession := range allSessions {
|
||||||
if targetSession.Mode == SessionModePending && currentSessionSettings != nil && currentSessionSettings.RequireApproval {
|
if existingSession.ID != sessionID && existingSession.Nickname == nickname {
|
||||||
if primary := sessionManager.GetPrimarySession(); primary != nil {
|
handlerErr = fmt.Errorf("nickname '%s' is already in use by another session", nickname)
|
||||||
go func() {
|
break
|
||||||
writeJSONRPCEvent("newSessionPending", map[string]interface{}{
|
|
||||||
"sessionId": targetSession.ID,
|
|
||||||
"source": targetSession.Source,
|
|
||||||
"identity": targetSession.Identity,
|
|
||||||
"nickname": targetSession.Nickname,
|
|
||||||
}, primary)
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionManager.broadcastSessionListUpdate()
|
if handlerErr == nil {
|
||||||
result = map[string]interface{}{"status": "updated"}
|
targetSession.Nickname = nickname
|
||||||
|
|
||||||
|
// If session is pending and approval is required, send the approval request now that we have a nickname
|
||||||
|
if targetSession.Mode == SessionModePending && currentSessionSettings != nil && currentSessionSettings.RequireApproval {
|
||||||
|
if primary := sessionManager.GetPrimarySession(); primary != nil {
|
||||||
|
go func() {
|
||||||
|
writeJSONRPCEvent("newSessionPending", map[string]interface{}{
|
||||||
|
"sessionId": targetSession.ID,
|
||||||
|
"source": targetSession.Source,
|
||||||
|
"identity": targetSession.Identity,
|
||||||
|
"nickname": targetSession.Nickname,
|
||||||
|
}, primary)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionManager.broadcastSessionListUpdate()
|
||||||
|
result = map[string]interface{}{"status": "updated"}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
handlerErr = errors.New("permission denied: can only update own nickname")
|
handlerErr = errors.New("permission denied: can only update own nickname")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,15 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
||||||
sm.mu.Lock()
|
sm.mu.Lock()
|
||||||
defer sm.mu.Unlock()
|
defer sm.mu.Unlock()
|
||||||
|
|
||||||
|
// Check nickname uniqueness (only for non-empty nicknames)
|
||||||
|
if session.Nickname != "" {
|
||||||
|
for id, existingSession := range sm.sessions {
|
||||||
|
if id != session.ID && existingSession.Nickname == session.Nickname {
|
||||||
|
return fmt.Errorf("nickname '%s' is already in use by another session", session.Nickname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
wasWithinGracePeriod := false
|
wasWithinGracePeriod := false
|
||||||
wasPreviouslyPrimary := false
|
wasPreviouslyPrimary := false
|
||||||
wasPreviouslyPending := false
|
wasPreviouslyPending := false
|
||||||
|
|
@ -195,12 +204,14 @@ func (sm *SessionManager) AddSession(session *Session, clientSettings *SessionSe
|
||||||
// If this was the primary, try to restore primary status
|
// If this was the primary, try to restore primary status
|
||||||
if existing.Mode == SessionModePrimary {
|
if existing.Mode == SessionModePrimary {
|
||||||
isBlacklisted := sm.isSessionBlacklisted(session.ID)
|
isBlacklisted := sm.isSessionBlacklisted(session.ID)
|
||||||
if sm.lastPrimaryID == session.ID && !isBlacklisted {
|
// SECURITY: Prevent dual-primary window - only restore if no other primary exists
|
||||||
|
primaryExists := sm.primarySessionID != "" && sm.sessions[sm.primarySessionID] != nil
|
||||||
|
if sm.lastPrimaryID == session.ID && !isBlacklisted && !primaryExists {
|
||||||
sm.primarySessionID = session.ID
|
sm.primarySessionID = session.ID
|
||||||
sm.lastPrimaryID = ""
|
sm.lastPrimaryID = ""
|
||||||
delete(sm.reconnectGrace, session.ID)
|
delete(sm.reconnectGrace, session.ID)
|
||||||
} else {
|
} else {
|
||||||
// Grace period expired or another session took over
|
// Grace period expired, another session took over, or primary already exists
|
||||||
session.Mode = SessionModeObserver
|
session.Mode = SessionModeObserver
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -781,6 +792,23 @@ func (sm *SessionManager) ApprovePrimaryRequest(currentPrimaryID, requesterID st
|
||||||
return errors.New("not the primary session")
|
return errors.New("not the primary session")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SECURITY: Verify requester session exists and is in Queued mode
|
||||||
|
requesterSession, exists := sm.sessions[requesterID]
|
||||||
|
if !exists {
|
||||||
|
sm.logger.Error().
|
||||||
|
Str("requesterID", requesterID).
|
||||||
|
Msg("Requester session not found")
|
||||||
|
return errors.New("requester session not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if requesterSession.Mode != SessionModeQueued {
|
||||||
|
sm.logger.Error().
|
||||||
|
Str("requesterID", requesterID).
|
||||||
|
Str("actualMode", string(requesterSession.Mode)).
|
||||||
|
Msg("Requester session is not in queued mode")
|
||||||
|
return fmt.Errorf("requester session is not in queued mode (current mode: %s)", requesterSession.Mode)
|
||||||
|
}
|
||||||
|
|
||||||
// Remove requester from queue
|
// Remove requester from queue
|
||||||
sm.removeFromQueue(requesterID)
|
sm.removeFromQueue(requesterID)
|
||||||
|
|
||||||
|
|
@ -838,6 +866,51 @@ func (sm *SessionManager) DenyPrimaryRequest(currentPrimaryID, requesterID strin
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ApproveSession approves a pending session (thread-safe)
|
||||||
|
func (sm *SessionManager) ApproveSession(sessionID string) error {
|
||||||
|
sm.mu.Lock()
|
||||||
|
defer sm.mu.Unlock()
|
||||||
|
|
||||||
|
session, exists := sm.sessions[sessionID]
|
||||||
|
if !exists {
|
||||||
|
return ErrSessionNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.Mode != SessionModePending {
|
||||||
|
return errors.New("session is not in pending mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Promote session to observer
|
||||||
|
session.Mode = SessionModeObserver
|
||||||
|
|
||||||
|
sm.logger.Info().
|
||||||
|
Str("sessionID", sessionID).
|
||||||
|
Msg("Session approved and promoted to observer")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DenySession denies a pending session (thread-safe)
|
||||||
|
func (sm *SessionManager) DenySession(sessionID string) error {
|
||||||
|
sm.mu.Lock()
|
||||||
|
defer sm.mu.Unlock()
|
||||||
|
|
||||||
|
session, exists := sm.sessions[sessionID]
|
||||||
|
if !exists {
|
||||||
|
return ErrSessionNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.Mode != SessionModePending {
|
||||||
|
return errors.New("session is not in pending mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
sm.logger.Info().
|
||||||
|
Str("sessionID", sessionID).
|
||||||
|
Msg("Session denied - notifying session")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ForEachSession executes a function for each active session
|
// ForEachSession executes a function for each active session
|
||||||
func (sm *SessionManager) ForEachSession(fn func(*Session)) {
|
func (sm *SessionManager) ForEachSession(fn func(*Session)) {
|
||||||
sm.mu.RLock()
|
sm.mu.RLock()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue