Use of Persistent Cookies Containing Sensitive Information
Description
Use of Persistent Cookies Containing Sensitive Information occurs when a web application stores sensitive data (session tokens, credentials, personal information, or security-relevant data) in cookies that persist beyond the browser session. Persistent cookies remain on the user's device until they expire or are explicitly deleted. This creates extended exposure windows where the sensitive data can be stolen through various attack vectors including physical access, malware, or cookie theft techniques.
Risk
Persistent cookies extend the attack window for sensitive data exposure significantly. On shared or public computers, subsequent users can access previous users' authenticated sessions. Malware can harvest persistent cookies from disk. If cookies lack proper security flags, they're vulnerable to interception and cross-site attacks. Session tokens in persistent cookies enable long-term session hijacking. This vulnerability is particularly dangerous for financial, healthcare, and other high-security applications where extended authentication poses significant risk.
Solution
Use session cookies (without Expires/Max-Age) for sensitive data so they're deleted when the browser closes. If persistence is required for user convenience (remember me), use a separate secure token with limited privileges rather than the full session. Set short expiration times for persistent cookies. Always use Secure and HttpOnly flags. Implement SameSite=Strict or Lax. Store only references (IDs) in cookies, keeping sensitive data server-side. Provide users with session management controls. Invalidate persistent tokens server-side on logout.
Common Consequences
| Impact | Details |
|---|---|
| Confidentiality | Scope: Data Exposure Sensitive information persists on disk where it can be accessed. |
| Authentication | Scope: Session Hijacking Persistent session tokens enable long-term unauthorized access. |
| Privacy | Scope: Tracking Persistent cookies enable extended user tracking and profiling. |
Example Code + Solution Code
Vulnerable Code
# VULNERABLE: Sensitive data in persistent cookie
from flask import Flask, request, make_response
@app.route('/login', methods=['POST'])
def login_vulnerable():
username = request.form['username']
password = request.form['password']
if authenticate(username, password):
response = make_response('Login successful')
# Session token in persistent cookie - bad!
response.set_cookie(
'session_token',
generate_session_token(),
max_age=30*24*60*60, # 30 days!
# Missing secure flags!
)
# Storing sensitive data directly!
response.set_cookie(
'user_data',
f"{username}:{user.role}:{user.email}",
max_age=30*24*60*60
)
return response
return 'Invalid credentials', 401
# VULNERABLE: Remember me with full session access
@app.route('/login', methods=['POST'])
def login_remember_vulnerable():
username = request.form['username']
password = request.form['password']
remember = request.form.get('remember', False)
if authenticate(username, password):
session_token = create_full_session(username)
response = make_response(redirect('/dashboard'))
if remember:
# Full session persisted for 30 days!
response.set_cookie(
'session',
session_token,
max_age=30*24*60*60, # 30 days
httponly=True # At least has this...
)
else:
response.set_cookie('session', session_token, httponly=True)
return response
// VULNERABLE: Java servlet with persistent sensitive cookies
@WebServlet("/login")
public class VulnerableLoginServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
if (authenticate(username, password)) {
// Persistent session cookie - bad!
Cookie sessionCookie = new Cookie("session", generateToken());
sessionCookie.setMaxAge(30 * 24 * 60 * 60); // 30 days
sessionCookie.setPath("/");
// Missing Secure and HttpOnly!
response.addCookie(sessionCookie);
// Storing user role in cookie!
Cookie roleCookie = new Cookie("role", getUserRole(username));
roleCookie.setMaxAge(30 * 24 * 60 * 60);
response.addCookie(roleCookie);
response.sendRedirect("/dashboard");
}
}
}
// VULNERABLE: Storing credentials in cookie
@WebServlet("/remember")
public class VulnerableRememberServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
if (authenticate(username, password)) {
// Storing credentials in persistent cookie - NEVER do this!
String encoded = Base64.getEncoder().encodeToString(
(username + ":" + password).getBytes()
);
Cookie rememberCookie = new Cookie("credentials", encoded);
rememberCookie.setMaxAge(365 * 24 * 60 * 60); // 1 year!
response.addCookie(rememberCookie);
}
}
}
// VULNERABLE: Express with persistent sensitive cookies
app.post('/login', (req, res) => {
const { username, password, remember } = req.body;
if (authenticate(username, password)) {
const token = generateSessionToken(username);
// Persistent cookie with full session!
res.cookie('session', token, {
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
httpOnly: true
// Missing secure and sameSite!
});
// Storing sensitive user data in cookie!
res.cookie('userData', JSON.stringify({
username,
email: user.email,
role: user.role
}), {
maxAge: 30 * 24 * 60 * 60 * 1000
});
res.redirect('/dashboard');
}
});
// VULNERABLE: Client-side persistent storage of sensitive data
function loginVulnerable(username, password) {
// Storing sensitive data in localStorage (worse than cookies!)
localStorage.setItem('username', username);
localStorage.setItem('authToken', token);
localStorage.setItem('role', user.role);
}
// VULNERABLE: Remember me with full session
app.post('/login', async (req, res) => {
const { username, password, rememberMe } = req.body;
const user = await authenticate(username, password);
if (user) {
const session = createSession(user);
// Full session token persisted
const cookieOptions = {
httpOnly: true,
secure: true
};
if (rememberMe) {
cookieOptions.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days
}
res.cookie('session', session.token, cookieOptions);
}
});
Fixed Code
# SAFE: Session cookies for sensitive data
from flask import Flask, request, make_response, session
import secrets
from datetime import datetime, timedelta
# SAFE: Use session cookies (no expiration = deleted when browser closes)
@app.route('/login', methods=['POST'])
def login_safe():
username = request.form['username']
password = request.form['password']
if authenticate(username, password):
response = make_response(redirect('/dashboard'))
# Session cookie - no max_age means it expires when browser closes
session_token = generate_session_token()
response.set_cookie(
'session',
session_token,
httponly=True,
secure=True,
samesite='Lax'
# No max_age = session cookie
)
# Store sensitive data server-side, not in cookie
store_session_data(session_token, {
'user_id': user.id,
'username': username,
'role': user.role,
'created': datetime.utcnow().isoformat()
})
return response
return 'Invalid credentials', 401
# SAFE: Separate "remember me" token with limited privileges
@app.route('/login', methods=['POST'])
def login_with_remember_safe():
username = request.form['username']
password = request.form['password']
remember = request.form.get('remember', False)
user = authenticate(username, password)
if not user:
return 'Invalid credentials', 401
response = make_response(redirect('/dashboard'))
# Session token (expires with browser)
session_token = create_session(user)
response.set_cookie(
'session',
session_token,
httponly=True,
secure=True,
samesite='Lax'
# No expiration = session cookie
)
if remember:
# Separate remember token with LIMITED privileges
remember_token = create_remember_token(user.id)
response.set_cookie(
'remember',
remember_token,
max_age=30*24*60*60, # 30 days
httponly=True,
secure=True,
samesite='Strict', # Stricter for persistent token
path='/auth/remember' # Limited path
)
# Store remember token hash server-side
store_remember_token(user.id, hash_token(remember_token))
return response
# Endpoint to restore session from remember token
@app.route('/auth/remember', methods=['POST'])
def restore_session():
remember_token = request.cookies.get('remember')
if not remember_token:
return 'No remember token', 401
# Validate remember token
user_id = validate_remember_token(remember_token)
if not user_id:
response = make_response('Invalid token', 401)
response.delete_cookie('remember')
return response
# Require re-authentication for sensitive operations
user = User.query.get(user_id)
response = make_response(redirect('/dashboard'))
# Create new session token
session_token = create_session(user, limited=True) # Limited session!
response.set_cookie('session', session_token, httponly=True, secure=True)
# Rotate remember token
new_remember = create_remember_token(user_id)
response.set_cookie('remember', new_remember, max_age=30*24*60*60,
httponly=True, secure=True, samesite='Strict')
return response
// SAFE: Java with proper cookie handling
@WebServlet("/login")
public class SecureLoginServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String username = request.getParameter("username");
String password = request.getParameter("password");
boolean remember = "true".equals(request.getParameter("remember"));
User user = authenticate(username, password);
if (user == null) {
response.sendRedirect("/login?error=invalid");
return;
}
// Session cookie - no MaxAge means browser session only
String sessionToken = generateSessionToken();
Cookie sessionCookie = new Cookie("session", sessionToken);
sessionCookie.setHttpOnly(true);
sessionCookie.setSecure(true);
sessionCookie.setPath("/");
// No setMaxAge = session cookie
response.addCookie(sessionCookie);
// Store session data server-side
SessionStore.save(sessionToken, user.getId(), user.getRole());
if (remember) {
// Separate remember-me token with limited scope
String rememberToken = generateRememberToken();
Cookie rememberCookie = new Cookie("remember", rememberToken);
rememberCookie.setHttpOnly(true);
rememberCookie.setSecure(true);
rememberCookie.setMaxAge(30 * 24 * 60 * 60); // 30 days
rememberCookie.setPath("/auth/remember"); // Limited path!
response.addCookie(rememberCookie);
// Store hash of remember token
RememberTokenStore.save(user.getId(), hashToken(rememberToken));
}
response.sendRedirect("/dashboard");
}
}
// SAFE: Spring Security configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.rememberMe(remember -> remember
// Use database-backed token repository
.tokenRepository(persistentTokenRepository())
// Short validity period
.tokenValiditySeconds(7 * 24 * 60 * 60) // 7 days
// Require re-auth for sensitive operations
.useSecureCookie(true)
)
.sessionManagement(session -> session
// Session cookies only for main authentication
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
);
return http.build();
}
@Bean
public PersistentTokenRepository persistentTokenRepository() {
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}
}
// SAFE: Require full authentication for sensitive operations
@Controller
public class SecureController {
@PreAuthorize("isFullyAuthenticated()") // Not remembered!
@PostMapping("/transfer")
public String transfer(@RequestBody TransferRequest request) {
// Only fully authenticated users can transfer funds
return processTransfer(request);
}
@PreAuthorize("isRememberMe() or isFullyAuthenticated()")
@GetMapping("/dashboard")
public String dashboard() {
// Dashboard accessible with remember-me
return "dashboard";
}
}
// SAFE: Express with proper cookie handling
const crypto = require('crypto');
app.post('/login', async (req, res) => {
const { username, password, rememberMe } = req.body;
const user = await authenticate(username, password);
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Session cookie - expires when browser closes
const sessionToken = generateSessionToken();
res.cookie('session', sessionToken, {
httpOnly: true,
secure: true,
sameSite: 'lax'
// No maxAge = session cookie
});
// Store session data server-side
await SessionStore.create(sessionToken, {
userId: user.id,
role: user.role,
createdAt: new Date()
});
if (rememberMe) {
// Separate remember token
const rememberToken = generateRememberToken();
const tokenHash = hashToken(rememberToken);
res.cookie('remember', rememberToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
path: '/auth/remember' // Limited path
});
// Store hash of token
await RememberTokenStore.create(user.id, tokenHash);
}
res.json({ success: true });
});
// Restore session from remember token
app.post('/auth/remember', async (req, res) => {
const rememberToken = req.cookies.remember;
if (!rememberToken) {
return res.status(401).json({ error: 'No remember token' });
}
const tokenHash = hashToken(rememberToken);
const stored = await RememberTokenStore.findByHash(tokenHash);
if (!stored) {
res.clearCookie('remember');
return res.status(401).json({ error: 'Invalid token' });
}
const user = await User.findById(stored.userId);
// Create LIMITED session
const sessionToken = generateSessionToken();
res.cookie('session', sessionToken, {
httpOnly: true,
secure: true,
sameSite: 'lax'
});
await SessionStore.create(sessionToken, {
userId: user.id,
role: user.role,
fromRemember: true, // Mark as restored session
createdAt: new Date()
});
// Rotate remember token
const newRememberToken = generateRememberToken();
await RememberTokenStore.update(stored.id, hashToken(newRememberToken));
res.cookie('remember', newRememberToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 30 * 24 * 60 * 60 * 1000,
path: '/auth/remember'
});
res.json({ success: true });
});
// Middleware to require full authentication for sensitive operations
const requireFullAuth = async (req, res, next) => {
const session = await SessionStore.find(req.cookies.session);
if (!session) {
return res.status(401).json({ error: 'Not authenticated' });
}
if (session.fromRemember) {
return res.status(403).json({
error: 'Full authentication required',
requireReauth: true
});
}
next();
};
app.post('/transfer', requireFullAuth, async (req, res) => {
// Only fully authenticated users can transfer
});
// Helper functions
function generateRememberToken() {
return crypto.randomBytes(32).toString('hex');
}
function hashToken(token) {
return crypto.createHash('sha256').update(token).digest('hex');
}
Exploited in the Wild
Session Hijacking via Persistent Cookies
Attackers have harvested persistent session cookies from shared computers and malware infections to hijack user accounts.
Credential Theft from Cookies
Applications storing credentials or sensitive tokens in persistent cookies have been exploited through cookie theft attacks.
Cross-Site Cookie Attacks
Persistent cookies without proper flags have been stolen through XSS and network interception attacks.
Tools to test/exploit
-
Browser Developer Tools — inspect cookie attributes.
-
Burp Suite — analyze cookie handling.
-
Cookie-Editor — browser extension for testing.
-
Network inspection tools for cookie interception.
CVE Examples
-
CVE-2019-11358 — Persistent cookie issues.
-
CVE-2020-8945 — Cookie-based session exposure.
-
Numerous application-specific cookie vulnerabilities.
References
-
MITRE. "CWE-539: Use of Persistent Cookies Containing Sensitive Information." https://cwe.mitre.org/data/definitions/539.html
-
OWASP. "Session Management Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html