fix(security): M2-008 Valkey key audit — SCAN over KEYS, restrict /gc to admin #298

Merged
jason.woltje merged 1 commits from fix/m2-valkey-keys-audit into main 2026-03-21 20:45:45 +00:00
Owner

Summary

  • Audit finding 1 — KEYS blocking: All three calls in (collect, sweepOrphans, fullCollect) used the Redis command, which blocks the Valkey event loop for the full scan duration. Replaced with a private helper that uses cursor-based iteration with COUNT 100.
  • Audit finding 2 — /gc cross-user scope: accepted a userId parameter but never used it, making the operation always system-wide. The slash command passed the calling user's ID but it was silently ignored — any authenticated user could trigger a global GC sweep, cleaning up other users' session state. Fixed by removing the unused param and promoting command scope from to .
  • Key patterns — no schema change needed: keys use UUID session IDs (cryptographically random, not guessable). Session ownership is enforced at the WebSocket/HTTP auth layer before any key access occurs, so embedding userId in the key is not required. uses tokens with 5-minute TTL; userId is stored in the value; no enumeration risk.

Test plan

@mosaic/gateway@0.0.0 typecheck /home/jwoltje/src/mosaic-mono-v1/apps/gateway
tsc --noEmit -p tsconfig.typecheck.json — passes

@mosaic/gateway@0.0.0 lint /home/jwoltje/src/mosaic-mono-v1/apps/gateway
eslint src — passes

mosaic-stack@ format:check /home/jwoltje/src/mosaic-mono-v1
prettier --check "**/*.{ts,tsx,js,jsx,json,md}"

Checking formatting...
All matched files use Prettier code style! — passes

@mosaic/gateway@0.0.0 test /home/jwoltje/src/mosaic-mono-v1/apps/gateway
vitest run --passWithNoTests

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
[Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Plugin registered: test-plugin
[Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Soft reload triggered by: command
[Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Reload complete. Reloaded: [test-plugin]. Errors: 0
[Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Plugin registered: bad-plugin
[Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Soft reload triggered by: command
[Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Reload complete. Reloaded: []. Errors: 1
[Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Plugin registered: not-a-plugin
[Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Soft reload triggered by: command
[Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Reload complete. Reloaded: []. Errors: 0
[Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Soft reload triggered by: rest
[Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Reload complete. Reloaded: []. Errors: 0
[Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Plugin registered: my-plugin
✓ src/reload/reload.service.spec.ts (5 tests) 12ms
✓ src/tests/resource-ownership.test.ts (7 tests) 15ms
[Nest] 343638 - 03/21/2026, 3:45:06 PM  DEBUG [PreferencesService] Upserted preference "agent.thinkingLevel" for user user-1
[Nest] 343638 - 03/21/2026, 3:45:06 PM  DEBUG [PreferencesService] Deleted preference "agent.thinkingLevel" for user user-1
✓ src/preferences/preferences.service.spec.ts (10 tests) 13ms
✓ src/auth/sso.controller.spec.ts (2 tests) 4ms
[Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping Anthropic provider registration: ANTHROPIC_API_KEY not set
[Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping OpenAI provider registration: OPENAI_API_KEY not set
[Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping Z.ai provider registration: ZAI_API_KEY not set
[Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Providers initialized: 0 models available
[Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Anthropic provider registered with 3 models
[Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping OpenAI provider registration: OPENAI_API_KEY not set
[Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping Z.ai provider registration: ZAI_API_KEY not set
[Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Providers initialized: 3 models available
[Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping Anthropic provider registration: ANTHROPIC_API_KEY not set
[Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] OpenAI provider registered with 3 models
[Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping Z.ai provider registration: ZAI_API_KEY not set
[Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Providers initialized: 3 models available
[Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping Anthropic provider registration: ANTHROPIC_API_KEY not set
[Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping OpenAI provider registration: OPENAI_API_KEY not set
[Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Z.ai provider registered with 3 models
[Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Providers initialized: 3 models available
[Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Anthropic provider registered with 3 models
[Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] OpenAI provider registered with 3 models
[Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Z.ai provider registered with 3 models
[Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Providers initialized: 9 models available
[Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Anthropic provider registered with 3 models
[Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping OpenAI provider registration: OPENAI_API_KEY not set
[Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping Z.ai provider registration: ZAI_API_KEY not set
[Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Providers initialized: 3 models available
[Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping Anthropic provider registration: ANTHROPIC_API_KEY not set
[Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping OpenAI provider registration: OPENAI_API_KEY not set
[Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Z.ai provider registered with 3 models
[Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Providers initialized: 3 models available
✓ src/agent/tests/provider.service.test.ts (7 tests) 23ms
[Nest] 343646 - 03/21/2026, 3:45:06 PM  DEBUG [RoutingService] Routed to anthropic/claude-3-haiku (score=60): base score
[Nest] 343646 - 03/21/2026, 3:45:06 PM  WARN [RoutingService] No available models for routing
[Nest] 343646 - 03/21/2026, 3:45:06 PM  DEBUG [RoutingService] Routed to anthropic/claude-3-sonnet (score=55): base score
[Nest] 343646 - 03/21/2026, 3:45:06 PM  DEBUG [RoutingService] Routed to anthropic/claude-3-haiku (score=60): base score
[Nest] 343646 - 03/21/2026, 3:45:06 PM  DEBUG [RoutingService] Routed to anthropic/claude-3-haiku (score=60): base score
[Nest] 343646 - 03/21/2026, 3:45:06 PM  DEBUG [RoutingService] Routed to anthropic/claude-3-haiku (score=70): cost tier match (cheap)
✓ 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

  • Updated to mock instead of
  • Updated to assert called without args

🤖 Generated with Claude Code

## Summary - **Audit finding 1 — KEYS blocking**: All three calls in (collect, sweepOrphans, fullCollect) used the Redis command, which blocks the Valkey event loop for the full scan duration. Replaced with a private helper that uses cursor-based iteration with COUNT 100. - **Audit finding 2 — /gc cross-user scope**: accepted a userId parameter but never used it, making the operation always system-wide. The slash command passed the calling user's ID but it was silently ignored — any authenticated user could trigger a global GC sweep, cleaning up other users' session state. Fixed by removing the unused param and promoting command scope from to . - **Key patterns — no schema change needed**: keys use UUID session IDs (cryptographically random, not guessable). Session ownership is enforced at the WebSocket/HTTP auth layer before any key access occurs, so embedding userId in the key is not required. uses tokens with 5-minute TTL; userId is stored in the value; no enumeration risk. ## Test plan - [x] > @mosaic/gateway@0.0.0 typecheck /home/jwoltje/src/mosaic-mono-v1/apps/gateway > tsc --noEmit -p tsconfig.typecheck.json — passes - [x] > @mosaic/gateway@0.0.0 lint /home/jwoltje/src/mosaic-mono-v1/apps/gateway > eslint src — passes - [x] > mosaic-stack@ format:check /home/jwoltje/src/mosaic-mono-v1 > prettier --check "**/*.{ts,tsx,js,jsx,json,md}" Checking formatting... All matched files use Prettier code style! — passes - [x] > @mosaic/gateway@0.0.0 test /home/jwoltje/src/mosaic-mono-v1/apps/gateway > vitest run --passWithNoTests 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 [Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Plugin registered: test-plugin [Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Soft reload triggered by: command [Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Reload complete. Reloaded: [test-plugin]. Errors: 0 [Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Plugin registered: bad-plugin [Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Soft reload triggered by: command [Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Reload complete. Reloaded: []. Errors: 1 [Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Plugin registered: not-a-plugin [Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Soft reload triggered by: command [Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Reload complete. Reloaded: []. Errors: 0 [Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Soft reload triggered by: rest [Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Reload complete. Reloaded: []. Errors: 0 [Nest] 343666 - 03/21/2026, 3:45:05 PM  LOG [ReloadService] Plugin registered: my-plugin ✓ src/reload/reload.service.spec.ts (5 tests) 12ms ✓ src/__tests__/resource-ownership.test.ts (7 tests) 15ms [Nest] 343638 - 03/21/2026, 3:45:06 PM  DEBUG [PreferencesService] Upserted preference "agent.thinkingLevel" for user user-1 [Nest] 343638 - 03/21/2026, 3:45:06 PM  DEBUG [PreferencesService] Deleted preference "agent.thinkingLevel" for user user-1 ✓ src/preferences/preferences.service.spec.ts (10 tests) 13ms ✓ src/auth/sso.controller.spec.ts (2 tests) 4ms [Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping Anthropic provider registration: ANTHROPIC_API_KEY not set [Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping OpenAI provider registration: OPENAI_API_KEY not set [Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping Z.ai provider registration: ZAI_API_KEY not set [Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Providers initialized: 0 models available [Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Anthropic provider registered with 3 models [Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping OpenAI provider registration: OPENAI_API_KEY not set [Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping Z.ai provider registration: ZAI_API_KEY not set [Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Providers initialized: 3 models available [Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping Anthropic provider registration: ANTHROPIC_API_KEY not set [Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] OpenAI provider registered with 3 models [Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping Z.ai provider registration: ZAI_API_KEY not set [Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Providers initialized: 3 models available [Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping Anthropic provider registration: ANTHROPIC_API_KEY not set [Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping OpenAI provider registration: OPENAI_API_KEY not set [Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Z.ai provider registered with 3 models [Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Providers initialized: 3 models available [Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Anthropic provider registered with 3 models [Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] OpenAI provider registered with 3 models [Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Z.ai provider registered with 3 models [Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Providers initialized: 9 models available [Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Anthropic provider registered with 3 models [Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping OpenAI provider registration: OPENAI_API_KEY not set [Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping Z.ai provider registration: ZAI_API_KEY not set [Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Providers initialized: 3 models available [Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping Anthropic provider registration: ANTHROPIC_API_KEY not set [Nest] 343640 - 03/21/2026, 3:45:06 PM  DEBUG [ProviderService] Skipping OpenAI provider registration: OPENAI_API_KEY not set [Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Z.ai provider registered with 3 models [Nest] 343640 - 03/21/2026, 3:45:06 PM  LOG [ProviderService] Providers initialized: 3 models available ✓ src/agent/__tests__/provider.service.test.ts (7 tests) 23ms [Nest] 343646 - 03/21/2026, 3:45:06 PM  DEBUG [RoutingService] Routed to anthropic/claude-3-haiku (score=60): base score [Nest] 343646 - 03/21/2026, 3:45:06 PM  WARN [RoutingService] No available models for routing [Nest] 343646 - 03/21/2026, 3:45:06 PM  DEBUG [RoutingService] Routed to anthropic/claude-3-sonnet (score=55): base score [Nest] 343646 - 03/21/2026, 3:45:06 PM  DEBUG [RoutingService] Routed to anthropic/claude-3-haiku (score=60): base score [Nest] 343646 - 03/21/2026, 3:45:06 PM  DEBUG [RoutingService] Routed to anthropic/claude-3-haiku (score=60): base score [Nest] 343646 - 03/21/2026, 3:45:06 PM  DEBUG [RoutingService] Routed to anthropic/claude-3-haiku (score=70): cost tier match (cheap) ✓ 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 - [x] Updated to mock instead of - [x] Updated to assert called without args 🤖 Generated with [Claude Code](https://claude.com/claude-code)
jason.woltje added 1 commit 2026-03-21 20:45:08 +00:00
fix(security): M2-008 Valkey key audit — replace KEYS with SCAN, restrict /gc to admin
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/pr/ci Pipeline was successful
f40d362b46
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>
jason.woltje merged commit 5b089392fd into main 2026-03-21 20:45:45 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#298