Files
goclaw/internal/tools/session_scope.go
T
viettranx a4df5a08e3 fix(security): prevent cross-group session data leak in cron jobs
Group-scoped agents could read sessions from other groups via session
tools (sessions_list, sessions_history, session_status, sessions_send)
because they only checked agent_key, not group context. This caused
cron jobs to leak data from unrelated groups into reports.

Add isSessionInScope() guard to all 4 session tools with colon-bounded
chatID matching. New share_sessions setting (default false) controls
cross-group visibility, following the same pattern as share_memory and
share_knowledge_graph. Web UI toggle and i18n strings included.

63 test cases covering guild/DM/group users, realistic Zalo IDs,
boundary exactness, multi-colon chatIDs, and the exact bug scenario.
2026-04-13 11:04:19 +07:00

54 lines
1.5 KiB
Go

package tools
import (
"context"
"strings"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
// isSessionInScope checks whether a target session key falls within
// the current execution scope. Group-scoped runs can only access
// sessions belonging to the same group, unless share_sessions is enabled.
func isSessionInScope(ctx context.Context, targetKey, currentKey string) bool {
// Shared sessions mode — all sessions visible.
if store.IsSharedSessions(ctx) {
return true
}
// Always allow own session.
if targetKey == currentKey {
return true
}
// Extract group chat ID from user context.
userID := store.UserIDFromContext(ctx)
chatID := extractGroupChatID(userID)
if chatID == "" {
return true // DM or non-group scope — no restriction.
}
// Allow sessions containing this group's chat ID.
// Colon-bounded match prevents partial numeric collisions.
// Matches patterns like:
// agent:X:channel:group:{chatID}
// agent:X:channel:group:{chatID}:topic:N
marker := ":" + chatID
return strings.HasSuffix(targetKey, marker) ||
strings.Contains(targetKey, marker+":")
}
// extractGroupChatID extracts the chat ID from a group-scoped userID.
// Format: "group:channel:chatID" -> "chatID".
// Returns "" for non-group users (DM, guild, etc.).
func extractGroupChatID(userID string) string {
if !strings.HasPrefix(userID, "group:") {
return ""
}
parts := strings.SplitN(userID, ":", 3)
if len(parts) < 3 {
return ""
}
return parts[2]
}