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
| Impact | Details |
|---|---|
| Confidentiality | Scope: 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
- MITRE Corporation. "CWE-524: Use of Cache Containing Sensitive Information." https://cwe.mitre.org/data/definitions/524.html
- OWASP. "Session Management Cheat Sheet - Cache Control."
- MDN Web Docs. "HTTP Caching."