# Cache respects TTL/expiration rules

- **Pattern:** `ab-001888` (`mobile-offline-storage.storage-security.cache-expiration`)
- **Severity:** low
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001888
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001888)

## Why it matters

A cache with no TTL enforcement retains data beyond its useful life — including sensitive content like user-specific feeds, prices, or session artifacts. CWE-922 (Insecure Storage of Sensitive Information) applies when cached data persists longer than the context that authorized it: a logged-out user's next session can read the previous user's cached content if the cache is never expired. ISO 25010:2011 reliability.recoverability is also impacted — stale data served from an indefinite cache is indistinguishable from fresh data, making correctness impossible to guarantee after server-side updates.

## Severity rationale

Low because cache staleness primarily causes UX and correctness issues rather than direct exploitation, though indefinite retention of session-scoped data is a secondary security concern.

## Remediation

Implement TTL checking on every cache read and a cleanup timer that evicts expired entries periodically. Cap TTLs at 86,400 seconds (24 hours) for any cached data.

```tsx
interface Entry<T> { data: T; storedAt: number; ttl: number; }

export class CacheManager<T> {
  private store = new Map<string, Entry<T>>();

  constructor(private defaultTTL = 5 * 60_000) {
    setInterval(() => this.sweep(), 60_000);
  }

  set(key: string, data: T, ttl = this.defaultTTL) {
    this.store.set(key, { data, storedAt: Date.now(), ttl });
  }

  get(key: string): T | undefined {
    const e = this.store.get(key);
    if (!e) return undefined;
    if (Date.now() - e.storedAt > e.ttl) { this.store.delete(key); return undefined; }
    return e.data;
  }

  private sweep() {
    const now = Date.now();
    for (const [k, e] of this.store) {
      if (now - e.storedAt > e.ttl) this.store.delete(k);
    }
  }
}
```

Always call `cache.invalidate(key)` immediately after a mutation rather than waiting for TTL expiry. On logout, call `cache.clear()` to prevent the next session from reading the previous user's data.

## Detection

- **ID:** `cache-expiration`
- **Severity:** `low`
- **What to look for:** Examine cache implementation. Look for TTL configuration, expiration checks, or cleanup logic. Verify cached data is refreshed periodically and doesn't persist indefinitely.
- **Pass criteria:** Count all cache entry creation points. Cache entries have a configurable TTL of no more than 86400 seconds (24 hours). Expired entries must not be served to the user. Old cache is cleaned up periodically or on app launch.
- **Fail criteria:** Cache has no expiration. Could serve outdated data indefinitely.
- **Skip (N/A) when:** Never — cache expiration is important for data freshness and security.
- **Detail on fail:** `"Cache has no TTL. User data cached on first load persists indefinitely even if user logs out or data changes remotely."`
- **Remediation:** Implement TTL with periodic cleanup:

  ```tsx
  interface CacheEntry<T> {
    data: T;
    timestamp: number;
    ttl: number;
    key: string;
  }

  export class CacheManager<T> {
    private entries = new Map<string, CacheEntry<T>>();
    private cleanupInterval: NodeJS.Timeout | null = null;

    constructor(private defaultTTL = 5 * 60 * 1000) {
      this.startCleanupTimer();
    }

    set(key: string, data: T, ttl = this.defaultTTL) {
      this.entries.set(key, {
        data,
        timestamp: Date.now(),
        ttl,
        key,
      });
    }

    get(key: string): T | null {
      const entry = this.entries.get(key);

      if (!entry) return null;

      const isExpired = Date.now() - entry.timestamp > entry.ttl;
      if (isExpired) {
        this.entries.delete(key);
        return null;
      }

      return entry.data;
    }

    private startCleanupTimer() {
      // Clean up expired entries every minute
      this.cleanupInterval = setInterval(() => {
        const now = Date.now();
        for (const [key, entry] of this.entries.entries()) {
          if (now - entry.timestamp > entry.ttl) {
            this.entries.delete(key);
          }
        }
      }, 60 * 1000);
    }

    destroy() {
      if (this.cleanupInterval) {
        clearInterval(this.cleanupInterval);
      }
    }
  }
  ```

## External references

- cwe CWE-922
- iso-25010 reliability.recoverability

Taxons: data-integrity

HTML version: https://auditbuffet.com/patterns/ab-001888
