Security [MEDIUM]: RBAC permissions cache 60 s — revocation propagates slowly across instances #57
Reference in New Issue
Block a user
Delete Branch "%!s()"
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?
Problem
_roleDefaultsCachein 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-localImpact
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
Parent Epic: #1
Source: Full-Codebase Security Audit 2026-04-16 (A-19)
Resolved in
e2dddd3on branchsecurity/audit-2026-04-17.Changes
packages/api/src/trpc.ts— shrinkROLE_DEFAULTS_TTLfrom 60s to 10s as fail-safe; publish/subscribe oncapakraken:rbac-invalidateso 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— afterupdateUserRole(when role actually changes),setUserPermissions,resetUserPermissions: delete everyActiveSessionrow for the affected user so the next request re-auths through the tRPCjticheck, and callinvalidateRoleDefaultsCache()to fan out the invalidationTests
rbac-cache-redis-pubsub.test.ts— FakeRedis pub/sub fan-out: receiving a message on the channel clears the cache so nextloadRoleDefaults()re-reads from DB;invalidateRoleDefaultsCache()publishes exactly once on the channeluser-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 neitherAcceptance: 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
jtino longer matches anActiveSessionrow.