Back to Blog
Best Practices

Cache Invalidation and Its Security Implications

There are only two hard things in Computer Science: cache invalidation and naming things. But when cache invalidation fails, the security implications can be severe.

AK
Alex Kumar
Staff Engineer
December 15, 20244 min read
cachingpermissionsperformancesecurity

Phil Karlton famously said there are only two hard things in Computer Science: cache invalidation and naming things. While naming conventions are frustrating, cache invalidation failures can have real security consequences.

The Security Problem with Caching

Caching improves performance by storing frequently accessed data. But when that data includes permissions or access control information, stale caches become security vulnerabilities.

A Common Scenario

Consider a multi-tenant SaaS application:

// User permissions are cached for performance
async function getUserPermissions(userId: string): Promise<Permissions> {
  const cached = await cache.get(`permissions:${userId}`);
  if (cached) return cached;

  const permissions = await db.permissions.findByUserId(userId);
  await cache.set(`permissions:${userId}`, permissions, { ttl: 300 }); // 5 minutes

  return permissions;
}

Now imagine this scenario:

  1. Admin removes a user from a team at 10:00 AM
  2. User's permissions were cached at 9:58 AM
  3. User continues to access team data until 10:03 AM (cache expiry)

That's a 5-minute window where a revoked user still has access. In security-sensitive contexts, this is unacceptable.

Common Cache Invalidation Failures

1. Forgetting to Invalidate on Role Changes

// Vulnerable: No cache invalidation
async function removeUserFromTeam(userId: string, teamId: string) {
  await db.teamMembers.delete({ userId, teamId });
  // Cache still contains old permissions!
}

2. Inconsistent Cache Keys

// Different parts of the app use different cache keys
const key1 = `permissions:${userId}`;
const key2 = `user:${userId}:permissions`;
const key3 = `perms-${userId}`;

// Invalidating one doesn't invalidate the others
await cache.delete(`permissions:${userId}`);
// But key2 and key3 still have stale data!

3. Multi-Level Cache Inconsistency

// Application has multiple cache layers
const inMemoryCache = new Map();
const redisCache = new Redis();

// Invalidation only clears Redis
await redisCache.del(`permissions:${userId}`);
// In-memory cache on other servers still has old data!

Solutions

1. Event-Driven Invalidation

// Publish events when permissions change
eventBus.on('user.permissions.changed', async ({ userId }) => {
  // Invalidate all cache keys for this user
  await cache.delete(`permissions:${userId}`);
  await cache.delete(`user:${userId}:*`); // Pattern-based deletion
});

async function removeUserFromTeam(userId: string, teamId: string) {
  await db.teamMembers.delete({ userId, teamId });
  eventBus.emit('user.permissions.changed', { userId });
}

2. Centralized Cache Key Management

// Single source of truth for cache keys
const CacheKeys = {
  userPermissions: (userId: string) => `permissions:v1:${userId}`,
  teamMembers: (teamId: string) => `team:v1:${teamId}:members`,
};

// Use everywhere
await cache.delete(CacheKeys.userPermissions(userId));

3. Cache Versioning

// Store a version number that increments on changes
async function getUserPermissions(userId: string): Promise<Permissions> {
  const version = await db.users.getPermissionVersion(userId);
  const cacheKey = `permissions:${userId}:v${version}`;

  const cached = await cache.get(cacheKey);
  if (cached) return cached;

  const permissions = await db.permissions.findByUserId(userId);
  await cache.set(cacheKey, permissions, { ttl: 3600 }); // Longer TTL is safe now

  return permissions;
}

async function updateUserPermissions(userId: string) {
  await db.users.incrementPermissionVersion(userId);
  // Old cache entries become orphaned and eventually expire
}

4. Short TTLs for Security-Critical Data

// Different TTLs based on data sensitivity
const TTL = {
  userProfile: 3600,        // 1 hour - low sensitivity
  userPermissions: 60,       // 1 minute - high sensitivity
  sessionData: 0,            // No caching - critical
};

5. Distributed Cache Invalidation

// Use pub/sub for multi-server invalidation
const pubsub = new Redis();

pubsub.subscribe('cache:invalidate', async (message) => {
  const { pattern } = JSON.parse(message);
  // Clear local in-memory cache
  localCache.deletePattern(pattern);
});

async function invalidateCache(pattern: string) {
  // Clear Redis
  await redisCache.deletePattern(pattern);
  // Notify all servers
  await pubsub.publish('cache:invalidate', JSON.stringify({ pattern }));
}

Testing Cache Invalidation

describe('Permission Cache Invalidation', () => {
  it('should immediately revoke access when user is removed from team', async () => {
    // Setup: User is team member
    const user = await createUser();
    const team = await createTeam();
    await addUserToTeam(user.id, team.id);

    // Prime the cache
    const permissions1 = await getUserPermissions(user.id);
    expect(permissions1.teams).toContain(team.id);

    // Remove user from team
    await removeUserFromTeam(user.id, team.id);

    // Permissions should be immediately updated
    const permissions2 = await getUserPermissions(user.id);
    expect(permissions2.teams).not.toContain(team.id);
  });
});

How SecurityChecks Detects These Issues

Our analyzer identifies:

  1. Missing cache invalidation - Permission changes without corresponding cache updates
  2. Inconsistent invalidation - Patterns that invalidate some but not all related caches
  3. Long TTLs on security data - Permissions cached with TTLs over recommended thresholds
  4. Missing pub/sub in distributed systems - Local cache clears without notifying other nodes

Summary

Cache invalidation for security-sensitive data requires:

  • Immediate invalidation on permission changes
  • Centralized cache key management to ensure consistency
  • Event-driven architecture to propagate changes
  • Distributed invalidation for multi-server deployments
  • Short TTLs as a safety net

Don't let stale caches become security holes. Audit your caching strategy today.

Enjoyed this article?

Subscribe to get the latest security insights delivered to your inbox.