fix(security): M2-008 Valkey key audit — SCAN over KEYS, restrict /gc to admin #298
Reference in New Issue
Block a user
Delete Branch "fix/m2-valkey-keys-audit"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Test plan
Checking formatting...
All matched files use Prettier code style! — passes
RUN v2.1.9 /home/jwoltje/src/mosaic-mono-v1/apps/gateway
✓ src/agent/tools/path-guard.test.ts (12 tests) 10ms
✓ src/gc/session-gc.service.spec.ts (8 tests) 11ms
✓ src/chat/tests/chat-security.test.ts (6 tests) 12ms
✓ src/commands/command-registry.service.spec.ts (6 tests) 6ms
✓ src/workspace/workspace.service.spec.ts (5 tests) 3ms
[32m[Nest] 343666 - [39m03/21/2026, 3:45:05 PM [32m LOG[39m [38;5;3m[ReloadService] [39m[32mPlugin registered: test-plugin[39m
[32m[Nest] 343666 - [39m03/21/2026, 3:45:05 PM [32m LOG[39m [38;5;3m[ReloadService] [39m[32mSoft reload triggered by: command[39m
[32m[Nest] 343666 - [39m03/21/2026, 3:45:05 PM [32m LOG[39m [38;5;3m[ReloadService] [39m[32mReload complete. Reloaded: [test-plugin]. Errors: 0[39m
[32m[Nest] 343666 - [39m03/21/2026, 3:45:05 PM [32m LOG[39m [38;5;3m[ReloadService] [39m[32mPlugin registered: bad-plugin[39m
[32m[Nest] 343666 - [39m03/21/2026, 3:45:05 PM [32m LOG[39m [38;5;3m[ReloadService] [39m[32mSoft reload triggered by: command[39m
[32m[Nest] 343666 - [39m03/21/2026, 3:45:05 PM [32m LOG[39m [38;5;3m[ReloadService] [39m[32mReload complete. Reloaded: []. Errors: 1[39m
[32m[Nest] 343666 - [39m03/21/2026, 3:45:05 PM [32m LOG[39m [38;5;3m[ReloadService] [39m[32mPlugin registered: not-a-plugin[39m
[32m[Nest] 343666 - [39m03/21/2026, 3:45:05 PM [32m LOG[39m [38;5;3m[ReloadService] [39m[32mSoft reload triggered by: command[39m
[32m[Nest] 343666 - [39m03/21/2026, 3:45:05 PM [32m LOG[39m [38;5;3m[ReloadService] [39m[32mReload complete. Reloaded: []. Errors: 0[39m
[32m[Nest] 343666 - [39m03/21/2026, 3:45:05 PM [32m LOG[39m [38;5;3m[ReloadService] [39m[32mSoft reload triggered by: rest[39m
[32m[Nest] 343666 - [39m03/21/2026, 3:45:05 PM [32m LOG[39m [38;5;3m[ReloadService] [39m[32mReload complete. Reloaded: []. Errors: 0[39m
[32m[Nest] 343666 - [39m03/21/2026, 3:45:05 PM [32m LOG[39m [38;5;3m[ReloadService] [39m[32mPlugin registered: my-plugin[39m
✓ src/reload/reload.service.spec.ts (5 tests) 12ms
✓ src/tests/resource-ownership.test.ts (7 tests) 15ms
[95m[Nest] 343638 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[PreferencesService] [39m[95mUpserted preference "agent.thinkingLevel" for user user-1[39m
[95m[Nest] 343638 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[PreferencesService] [39m[95mDeleted preference "agent.thinkingLevel" for user user-1[39m
✓ src/preferences/preferences.service.spec.ts (10 tests) 13ms
✓ src/auth/sso.controller.spec.ts (2 tests) 4ms
[95m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[ProviderService] [39m[95mSkipping Anthropic provider registration: ANTHROPIC_API_KEY not set[39m
[95m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[ProviderService] [39m[95mSkipping OpenAI provider registration: OPENAI_API_KEY not set[39m
[95m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[ProviderService] [39m[95mSkipping Z.ai provider registration: ZAI_API_KEY not set[39m
[32m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [32m LOG[39m [38;5;3m[ProviderService] [39m[32mProviders initialized: 0 models available[39m
[32m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [32m LOG[39m [38;5;3m[ProviderService] [39m[32mAnthropic provider registered with 3 models[39m
[95m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[ProviderService] [39m[95mSkipping OpenAI provider registration: OPENAI_API_KEY not set[39m
[95m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[ProviderService] [39m[95mSkipping Z.ai provider registration: ZAI_API_KEY not set[39m
[32m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [32m LOG[39m [38;5;3m[ProviderService] [39m[32mProviders initialized: 3 models available[39m
[95m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[ProviderService] [39m[95mSkipping Anthropic provider registration: ANTHROPIC_API_KEY not set[39m
[32m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [32m LOG[39m [38;5;3m[ProviderService] [39m[32mOpenAI provider registered with 3 models[39m
[95m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[ProviderService] [39m[95mSkipping Z.ai provider registration: ZAI_API_KEY not set[39m
[32m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [32m LOG[39m [38;5;3m[ProviderService] [39m[32mProviders initialized: 3 models available[39m
[95m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[ProviderService] [39m[95mSkipping Anthropic provider registration: ANTHROPIC_API_KEY not set[39m
[95m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[ProviderService] [39m[95mSkipping OpenAI provider registration: OPENAI_API_KEY not set[39m
[32m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [32m LOG[39m [38;5;3m[ProviderService] [39m[32mZ.ai provider registered with 3 models[39m
[32m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [32m LOG[39m [38;5;3m[ProviderService] [39m[32mProviders initialized: 3 models available[39m
[32m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [32m LOG[39m [38;5;3m[ProviderService] [39m[32mAnthropic provider registered with 3 models[39m
[32m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [32m LOG[39m [38;5;3m[ProviderService] [39m[32mOpenAI provider registered with 3 models[39m
[32m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [32m LOG[39m [38;5;3m[ProviderService] [39m[32mZ.ai provider registered with 3 models[39m
[32m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [32m LOG[39m [38;5;3m[ProviderService] [39m[32mProviders initialized: 9 models available[39m
[32m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [32m LOG[39m [38;5;3m[ProviderService] [39m[32mAnthropic provider registered with 3 models[39m
[95m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[ProviderService] [39m[95mSkipping OpenAI provider registration: OPENAI_API_KEY not set[39m
[95m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[ProviderService] [39m[95mSkipping Z.ai provider registration: ZAI_API_KEY not set[39m
[32m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [32m LOG[39m [38;5;3m[ProviderService] [39m[32mProviders initialized: 3 models available[39m
[95m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[ProviderService] [39m[95mSkipping Anthropic provider registration: ANTHROPIC_API_KEY not set[39m
[95m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[ProviderService] [39m[95mSkipping OpenAI provider registration: OPENAI_API_KEY not set[39m
[32m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [32m LOG[39m [38;5;3m[ProviderService] [39m[32mZ.ai provider registered with 3 models[39m
[32m[Nest] 343640 - [39m03/21/2026, 3:45:06 PM [32m LOG[39m [38;5;3m[ProviderService] [39m[32mProviders initialized: 3 models available[39m
✓ src/agent/tests/provider.service.test.ts (7 tests) 23ms
[95m[Nest] 343646 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[RoutingService] [39m[95mRouted to anthropic/claude-3-haiku (score=60): base score[39m
[33m[Nest] 343646 - [39m03/21/2026, 3:45:06 PM [33m WARN[39m [38;5;3m[RoutingService] [39m[33mNo available models for routing[39m
[95m[Nest] 343646 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[RoutingService] [39m[95mRouted to anthropic/claude-3-sonnet (score=55): base score[39m
[95m[Nest] 343646 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[RoutingService] [39m[95mRouted to anthropic/claude-3-haiku (score=60): base score[39m
[95m[Nest] 343646 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[RoutingService] [39m[95mRouted to anthropic/claude-3-haiku (score=60): base score[39m
[95m[Nest] 343646 - [39m03/21/2026, 3:45:06 PM [95m DEBUG[39m [38;5;3m[RoutingService] [39m[95mRouted to anthropic/claude-3-haiku (score=70): cost tier match (cheap)[39m
✓ src/agent/tests/routing.service.test.ts (10 tests) 8ms
✓ src/commands/command-executor-p8012.spec.ts (14 tests) 6ms
✓ src/commands/commands.integration.spec.ts (42 tests) 11ms
Test Files 13 passed (13)
Tests 134 passed (134)
Start at 15:45:04
Duration 2.14s (transform 1.14s, setup 0ms, collect 9.76s, tests 134ms, environment 3ms, prepare 1.17s) — 134/134 tests pass
🤖 Generated with Claude Code
Audit findings: - mosaic:session:{sessionId}:* — session-scoped; sessionId is a UUID (not guessable); keys don't need userId embedded because session-ID ownership is enforced at the WebSocket/HTTP auth layer before any key access occurs - mosaic:auth:poll:{token} — token is crypto.randomUUID(); userId is stored in the value (not the key); TTL 5 min; no enumeration risk - sweepOrphans() accepted a _userId param but ignored it, allowing any authenticated user to trigger a system-wide GC sweep via /gc; fixed by removing the unused param and promoting /gc command scope to 'admin' - All three KEYS calls (collect, sweepOrphans, fullCollect) replaced with a private scanKeys() helper using SCAN cursor iteration to avoid Valkey event-loop stalls under production key volumes No key-pattern schema changes needed: session keys are already sufficiently opaque (UUID entropy). The cross-user action risk was in /gc dispatch scope. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>