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.
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.
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.
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.
ID: mobile-offline-storage.storage-security.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:
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);
}
}
}