Use of Cache Containing Sensitive Information

Description

Use of Cache Containing Sensitive Information is a vulnerability where code uses a cache that contains sensitive information, but the cache can be read by actors outside of the intended control sphere. Applications leverage caches to enhance efficiency when interacting with remote entities or performing resource-intensive operations. These caches store objects, threads, connections, pages, financial data, passwords, or similar resources to minimize initialization time. When unauthorized parties can access the cache through insufficient access controls, insecure storage, or cache exposure mechanisms, attackers may retrieve sensitive data.

Risk

Caching sensitive data without proper protection creates confidentiality risks. Browser caches may store authentication credentials, session tokens, or personal data that persists after logout. Shared server caches may expose one user's data to another. CDN or proxy caches might store sensitive API responses accessible to other users. In-memory caches can be dumped through memory disclosure vulnerabilities. Database query caches may retain sensitive query results beyond their intended lifetime. Cache timing attacks can reveal the presence or absence of sensitive data. The risk increases in multi-tenant environments where cache isolation is critical.

Solution

Implement strong access controls on all cache systems. Avoid caching sensitive data when possible - use "no-store" and "no-cache" directives for sensitive HTTP responses. Encrypt cached content at rest and in transit. Implement proper cache isolation in multi-tenant systems. Use short TTLs for cached sensitive data with proper invalidation. Clear caches on logout and session termination. Apply the principle of least privilege to cache access. Use separate cache instances for sensitive and non-sensitive data. Implement cache-busting for sensitive resources.

Common Consequences

ImpactDetails
ConfidentialityScope: Confidentiality

Read Application Data - Unauthorized access to cached sensitive information can expose credentials, personal data, financial information, or other confidential data to attackers.

Example Code

Vulnerable Code

// Vulnerable: Caching sensitive data without protection
public class VulnerableCacheService {
    // Vulnerable: Shared cache for all users
    private static final Map<String, Object> globalCache = new HashMap<>();

    public void cacheUserData(String userId, UserProfile profile) {
        // Vulnerable: Sensitive data cached without encryption
        globalCache.put("profile:" + userId, profile);

        // Vulnerable: Password cached in plaintext
        globalCache.put("credentials:" + userId, profile.getPassword());

        // Vulnerable: Financial data cached
        globalCache.put("balance:" + userId, profile.getAccountBalance());
    }

    public UserProfile getCachedProfile(String userId) {
        // Vulnerable: No access control check
        return (UserProfile) globalCache.get("profile:" + userId);
    }

    // Vulnerable: Anyone can enumerate cached users
    public Set<String> getCachedUserIds() {
        return globalCache.keySet();
    }
}

// Vulnerable: Database query cache with sensitive data
public class VulnerableQueryCache {

    private final Cache<String, ResultSet> queryCache =
        CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(1, TimeUnit.HOURS)  // Long TTL for sensitive data
            .build();

    public ResultSet executeQuery(String query) {
        // Vulnerable: Caches queries containing sensitive data
        return queryCache.get(query, () -> database.execute(query));
    }

    // "SELECT * FROM users WHERE ssn = '123-45-6789'" gets cached!
}
# Vulnerable: Flask application with insecure caching
from flask import Flask, request, session
from functools import lru_cache

app = Flask(__name__)

# Vulnerable: Global cache accessible to all requests
user_data_cache = {}

@lru_cache(maxsize=1000)
def get_user_profile(user_id):
    # Vulnerable: Sensitive profile data cached globally
    # Any user can potentially access through cache timing
    return database.get_user(user_id)

@app.route('/profile/<user_id>')
def vulnerable_profile(user_id):
    # Vulnerable: No authorization check before cache lookup
    profile = get_user_profile(user_id)
    return jsonify(profile)

# Vulnerable: Caching session data insecurely
@app.route('/login', methods=['POST'])
def vulnerable_login():
    user = authenticate(request.form['username'], request.form['password'])
    if user:
        # Vulnerable: Session token cached in shared dict
        user_data_cache[user.id] = {
            'session_token': generate_token(),
            'password_hash': user.password_hash,  # Sensitive!
            'credit_card': user.credit_card       # Very sensitive!
        }
    return redirect('/dashboard')
// Vulnerable: Express.js with insecure response caching
const express = require('express');
const app = express();

// Vulnerable: No cache control headers for sensitive responses
app.get('/api/user/profile', (req, res) => {
    const userId = req.session.userId;
    const profile = getUserProfile(userId);

    // Vulnerable: Default caching allows browser/proxy caching
    // No Cache-Control headers set
    res.json({
        username: profile.username,
        email: profile.email,
        ssn: profile.ssn,           // Sensitive!
        creditScore: profile.creditScore  // Sensitive!
    });
});

// Vulnerable: API key cached in browser
app.get('/api/config', (req, res) => {
    res.json({
        apiKey: process.env.API_KEY,  // Secret exposed and cached!
        secretToken: process.env.SECRET
    });
});

// Vulnerable: Shared Redis cache without user isolation
const redis = require('redis').createClient();

app.get('/api/transactions', async (req, res) => {
    const userId = req.session.userId;
    const cacheKey = `transactions`;  // Vulnerable: Not user-specific!

    let data = await redis.get(cacheKey);
    if (!data) {
        data = await getTransactions(userId);
        await redis.set(cacheKey, JSON.stringify(data), 'EX', 3600);
    }
    res.json(JSON.parse(data));
});
<!-- Vulnerable: HTML response without cache protection -->
<!DOCTYPE html>
<html>
<head>
    <!-- Vulnerable: No cache-control meta tags -->
    <title>User Dashboard</title>
</head>
<body>
    <!-- Sensitive data that may be cached -->
    <div class="user-info">
        <p>Welcome, John Doe</p>
        <p>Account: 1234-5678-9012-3456</p>
        <p>Balance: $10,234.56</p>
    </div>
</body>
</html>

Fixed Code

// Fixed: Secure caching with access controls and encryption
public class SecureCacheService {
    private final EncryptionService encryption;
    private final Cache<String, byte[]> secureCache;

    public SecureCacheService(EncryptionService encryption) {
        this.encryption = encryption;
        // Fixed: Short TTL and size limits
        this.secureCache = CacheBuilder.newBuilder()
            .maximumSize(100)
            .expireAfterWrite(5, TimeUnit.MINUTES)  // Short TTL
            .removalListener(this::secureClear)
            .build();
    }

    public void cacheUserData(String userId, UserProfile profile,
                              SecurityContext ctx) {
        // Fixed: Verify authorization
        if (!ctx.canAccessUser(userId)) {
            throw new UnauthorizedException("Cannot cache data for this user");
        }

        // Fixed: Never cache passwords
        UserProfileCached cached = new UserProfileCached(profile);
        // Remove sensitive fields
        cached.setPassword(null);
        cached.setSsn(null);

        // Fixed: Encrypt before caching
        byte[] encrypted = encryption.encrypt(
            serialize(cached),
            ctx.getUserKey()  // User-specific encryption key
        );

        // Fixed: User-specific cache key
        String cacheKey = generateSecureKey(ctx.getUserId(), "profile");
        secureCache.put(cacheKey, encrypted);
    }

    public UserProfile getCachedProfile(String userId, SecurityContext ctx) {
        // Fixed: Authorization check
        if (!ctx.canAccessUser(userId)) {
            throw new UnauthorizedException();
        }

        String cacheKey = generateSecureKey(ctx.getUserId(), "profile");
        byte[] encrypted = secureCache.getIfPresent(cacheKey);

        if (encrypted == null) {
            return null;
        }

        // Fixed: Decrypt with user's key
        byte[] decrypted = encryption.decrypt(encrypted, ctx.getUserKey());
        return deserialize(decrypted);
    }

    // Fixed: Secure cache key generation
    private String generateSecureKey(String userId, String type) {
        return HashUtil.sha256(userId + ":" + type + ":" + secretSalt);
    }

    // Fixed: Secure cleanup on removal
    private void secureClear(RemovalNotification<String, byte[]> notification) {
        byte[] value = notification.getValue();
        if (value != null) {
            Arrays.fill(value, (byte) 0);  // Zero out memory
        }
    }

    // Fixed: Clear all user data on logout
    public void clearUserCache(SecurityContext ctx) {
        String prefix = HashUtil.sha256(ctx.getUserId() + ":");
        secureCache.asMap().keySet().removeIf(k -> k.startsWith(prefix));
    }
}
# Fixed: Secure caching in Flask
from flask import Flask, request, session, make_response
from functools import wraps
import redis
from cryptography.fernet import Fernet

app = Flask(__name__)
redis_client = redis.Redis()

class SecureCache:
    def __init__(self, encryption_key):
        self.fernet = Fernet(encryption_key)

    def get(self, user_id, key):
        # Fixed: User-specific cache key
        cache_key = f"user:{user_id}:{key}"
        encrypted = redis_client.get(cache_key)
        if encrypted:
            return self.fernet.decrypt(encrypted)
        return None

    def set(self, user_id, key, value, ttl=300):  # Fixed: Short TTL
        cache_key = f"user:{user_id}:{key}"
        encrypted = self.fernet.encrypt(value.encode())
        redis_client.setex(cache_key, ttl, encrypted)

    def clear_user(self, user_id):
        # Fixed: Clear all user's cached data
        pattern = f"user:{user_id}:*"
        for key in redis_client.scan_iter(pattern):
            redis_client.delete(key)

secure_cache = SecureCache(app.config['CACHE_KEY'])

def no_cache(f):
    """Fixed: Decorator to prevent caching sensitive responses"""
    @wraps(f)
    def decorated_function(*args, **kwargs):
        response = make_response(f(*args, **kwargs))
        response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
        response.headers['Pragma'] = 'no-cache'
        response.headers['Expires'] = '0'
        return response
    return decorated_function

@app.route('/profile')
@no_cache  # Fixed: Prevent caching
def secure_profile():
    user_id = session.get('user_id')
    if not user_id:
        return "Unauthorized", 401

    profile = get_user_profile(user_id)

    # Fixed: Strip sensitive data from response
    safe_profile = {
        'username': profile['username'],
        'email': profile['email'],
        # SSN and credit score not included
    }
    return jsonify(safe_profile)

@app.route('/logout', methods=['POST'])
def logout():
    user_id = session.get('user_id')
    if user_id:
        # Fixed: Clear user's cache on logout
        secure_cache.clear_user(user_id)
    session.clear()
    return redirect('/')
// Fixed: Express.js with proper cache controls
const express = require('express');
const helmet = require('helmet');
const app = express();

// Fixed: Use helmet for security headers
app.use(helmet());

// Fixed: Middleware to prevent caching sensitive routes
function noCacheSensitive(req, res, next) {
    res.set({
        'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
        'Pragma': 'no-cache',
        'Expires': '0',
        'Surrogate-Control': 'no-store'
    });
    next();
}

// Fixed: Apply to sensitive endpoints
app.get('/api/user/profile', noCacheSensitive, (req, res) => {
    const userId = req.session.userId;
    if (!userId) {
        return res.status(401).json({ error: 'Unauthorized' });
    }

    const profile = getUserProfile(userId);

    // Fixed: Return only non-sensitive data
    res.json({
        username: profile.username,
        displayName: profile.displayName
        // No SSN, credit score, or financial data
    });
});

// Fixed: Separate cache per user with encryption
const crypto = require('crypto');

class SecureUserCache {
    constructor(redisClient, encryptionKey) {
        this.redis = redisClient;
        this.key = encryptionKey;
    }

    async get(userId, dataKey) {
        const cacheKey = this.generateKey(userId, dataKey);
        const encrypted = await this.redis.get(cacheKey);
        if (encrypted) {
            return this.decrypt(encrypted);
        }
        return null;
    }

    async set(userId, dataKey, data, ttlSeconds = 300) {
        const cacheKey = this.generateKey(userId, dataKey);
        const encrypted = this.encrypt(JSON.stringify(data));
        await this.redis.setex(cacheKey, ttlSeconds, encrypted);
    }

    async clearUser(userId) {
        const pattern = `user:${userId}:*`;
        const keys = await this.redis.keys(pattern);
        if (keys.length > 0) {
            await this.redis.del(...keys);
        }
    }

    generateKey(userId, dataKey) {
        const hash = crypto.createHmac('sha256', this.key)
            .update(`${userId}:${dataKey}`)
            .digest('hex');
        return `user:${userId}:${hash}`;
    }

    encrypt(data) {
        const iv = crypto.randomBytes(16);
        const cipher = crypto.createCipheriv('aes-256-gcm', this.key, iv);
        let encrypted = cipher.update(data, 'utf8', 'hex');
        encrypted += cipher.final('hex');
        const authTag = cipher.getAuthTag();
        return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
    }

    decrypt(data) {
        const [ivHex, authTagHex, encrypted] = data.split(':');
        const iv = Buffer.from(ivHex, 'hex');
        const authTag = Buffer.from(authTagHex, 'hex');
        const decipher = crypto.createDecipheriv('aes-256-gcm', this.key, iv);
        decipher.setAuthTag(authTag);
        let decrypted = decipher.update(encrypted, 'hex', 'utf8');
        decrypted += decipher.final('utf8');
        return JSON.parse(decrypted);
    }
}
<!-- Fixed: HTML with cache protection -->
<!DOCTYPE html>
<html>
<head>
    <!-- Fixed: Prevent caching of sensitive pages -->
    <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
    <meta http-equiv="Pragma" content="no-cache">
    <meta http-equiv="Expires" content="0">
    <title>User Dashboard</title>
</head>
<body>
    <!-- Fixed: Sensitive data loaded via authenticated API call, not in HTML -->
    <div class="user-info" id="user-info">
        <!-- Data populated by JavaScript from secured API -->
    </div>
    <script>
        // Fixed: Fetch sensitive data dynamically, not embedded in cacheable HTML
        fetch('/api/user/profile', { credentials: 'include' })
            .then(response => response.json())
            .then(data => {
                document.getElementById('user-info').innerHTML =
                    `<p>Welcome, ${escapeHtml(data.displayName)}</p>`;
            });
    </script>
</body>
</html>

CVE Examples

No specific CVEs are listed in the MITRE database for this CWE. However, the vulnerability pattern is documented in:

  • CAPEC-204: Lifting Sensitive Data Embedded in Cache
  • Various web application security advisories

References

  1. MITRE Corporation. "CWE-524: Use of Cache Containing Sensitive Information." https://cwe.mitre.org/data/definitions/524.html
  2. OWASP. "Session Management Cheat Sheet - Cache Control."
  3. MDN Web Docs. "HTTP Caching."