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

ImpactDetails
ConfidentialityScope: Data Exposure

Sensitive information persists on disk where it can be accessed.
AuthenticationScope: Session Hijacking

Persistent session tokens enable long-term unauthorized access.
PrivacyScope: 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.

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

  1. MITRE. "CWE-539: Use of Persistent Cookies Containing Sensitive Information." https://cwe.mitre.org/data/definitions/539.html

  2. OWASP. "Session Management Cheat Sheet." https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html