Security [MEDIUM]: RBAC permissions cache 60 s — revocation propagates slowly across instances #57

Closed
opened 2026-04-16 22:05:12 +02:00 by Hartmut · 1 comment
Owner

Problem

_roleDefaultsCache in trpc.ts is process-local with 60 s TTL. After permission revocation, a compromised user keeps old permissions for up to 60 s. In multi-instance deployments, invalidateRoleDefaultsCache() only clears on the invoking node — other nodes remain stale.

Evidence

  • packages/api/src/trpc.ts:25-50 — ROLE_DEFAULTS_TTL = 60_000, module-local

Impact

Window of 60 s (single node) or longer (multi-node) where revoked permissions remain active. Acceptable in low-privilege revocations, risky for admin demotions.

Proposed Fix

(1) Publish cache-invalidation event via Redis pub/sub on permission changes — already have Redis. All nodes subscribe and clear local cache. (2) Reduce TTL to 10 s for security-sensitive keys. (3) For admin demotions specifically, also invalidate all active sessions of the target user.

Acceptance Criteria

  • Redis pub/sub invalidation in place
  • Multi-instance revocation test: 2nd node sees update within 1 s
  • Admin demotion forces user re-login

Parent Epic: #1
Source: Full-Codebase Security Audit 2026-04-16 (A-19)

## Problem `_roleDefaultsCache` in trpc.ts is process-local with 60 s TTL. After permission revocation, a compromised user keeps old permissions for up to 60 s. In multi-instance deployments, `invalidateRoleDefaultsCache()` only clears on the invoking node — other nodes remain stale. ## Evidence - `packages/api/src/trpc.ts:25-50 — ROLE_DEFAULTS_TTL = 60_000, module-local` ## Impact Window of 60 s (single node) or longer (multi-node) where revoked permissions remain active. Acceptable in low-privilege revocations, risky for admin demotions. ## Proposed Fix (1) Publish cache-invalidation event via Redis pub/sub on permission changes — already have Redis. All nodes subscribe and clear local cache. (2) Reduce TTL to 10 s for security-sensitive keys. (3) For admin demotions specifically, also invalidate all active sessions of the target user. ## Acceptance Criteria - [ ] Redis pub/sub invalidation in place - [ ] Multi-instance revocation test: 2nd node sees update within 1 s - [ ] Admin demotion forces user re-login --- Parent Epic: #1 Source: Full-Codebase Security Audit 2026-04-16 (A-19)
Hartmut added the security label 2026-04-16 22:05:12 +02:00
Author
Owner

Resolved in e2dddd3 on branch security/audit-2026-04-17.

Changes

  • packages/api/src/trpc.ts — shrink ROLE_DEFAULTS_TTL from 60s to 10s as fail-safe; publish/subscribe on capakraken:rbac-invalidate so peer instances drop their local cache on mutation (lazy-init ioredis client so modules that never touch RBAC do not open a connection)
  • packages/api/src/router/user-procedure-support.ts — after updateUserRole (when role actually changes), setUserPermissions, resetUserPermissions: delete every ActiveSession row for the affected user so the next request re-auths through the tRPC jti check, and call invalidateRoleDefaultsCache() to fan out the invalidation

Tests

  • rbac-cache-redis-pubsub.test.ts — FakeRedis pub/sub fan-out: receiving a message on the channel clears the cache so next loadRoleDefaults() re-reads from DB; invalidateRoleDefaultsCache() publishes exactly once on the channel
  • user-role-permission-session-invalidation.test.ts — mutation side-effects assert session deletion + cache invalidation on each privileged mutation path, and verify the no-op path (unchanged role) does neither

Acceptance: within at most 10s (and typically sub-second via pub/sub) a role/permission change invalidates the role-defaults cache on every node; any existing JWT for the affected user is rejected at the next request because its jti no longer matches an ActiveSession row.

Resolved in e2dddd3 on branch `security/audit-2026-04-17`. **Changes** - `packages/api/src/trpc.ts` — shrink `ROLE_DEFAULTS_TTL` from 60s to 10s as fail-safe; publish/subscribe on `capakraken:rbac-invalidate` so peer instances drop their local cache on mutation (lazy-init ioredis client so modules that never touch RBAC do not open a connection) - `packages/api/src/router/user-procedure-support.ts` — after `updateUserRole` (when role actually changes), `setUserPermissions`, `resetUserPermissions`: delete every `ActiveSession` row for the affected user so the next request re-auths through the tRPC `jti` check, and call `invalidateRoleDefaultsCache()` to fan out the invalidation **Tests** - `rbac-cache-redis-pubsub.test.ts` — FakeRedis pub/sub fan-out: receiving a message on the channel clears the cache so next `loadRoleDefaults()` re-reads from DB; `invalidateRoleDefaultsCache()` publishes exactly once on the channel - `user-role-permission-session-invalidation.test.ts` — mutation side-effects assert session deletion + cache invalidation on each privileged mutation path, and verify the no-op path (unchanged role) does neither Acceptance: within at most 10s (and typically sub-second via pub/sub) a role/permission change invalidates the role-defaults cache on every node; any existing JWT for the affected user is rejected at the next request because its `jti` no longer matches an `ActiveSession` row.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: Hartmut/CapaKraken#57