Use of Web Browser Cache Containing Sensitive Information

Description

Use of Web Browser Cache Containing Sensitive Information occurs when a web application allows sensitive data to be stored in browser cache, history, or client-side storage mechanisms. This includes caching pages with personal information, storing sensitive data in localStorage/sessionStorage, allowing autocomplete on sensitive form fields, and failing to set appropriate cache-control headers. Sensitive data persists on client devices where it can be accessed by other users or malware.

Risk

Cached sensitive information creates multiple attack vectors. On shared computers (libraries, kiosks, family devices), subsequent users can access previous users' cached data. Browser history exposes sensitive URLs. Autocomplete reveals previous form inputs including passwords and credit cards. localStorage persists even after sessions end. Cached responses may be accessed through browser developer tools or filesystem inspection. This risk is amplified for applications handling financial, medical, or personal data.

Solution

Set appropriate cache-control headers to prevent caching of sensitive pages: Cache-Control: no-store, no-cache, must-revalidate, private. Add Pragma: no-cache for HTTP/1.0 compatibility. Disable autocomplete on sensitive form fields using autocomplete="off" or specific values like autocomplete="new-password". Avoid storing sensitive data in localStorage/sessionStorage—use httpOnly cookies or server-side sessions instead. Clear sensitive data when users log out. Use memory-only storage for sensitive client-side data.

Common Consequences

ImpactDetails
ConfidentialityScope: Information Disclosure

Sensitive data accessible to subsequent users of shared devices.
PrivacyScope: Personal Data Exposure

PII, financial data, and medical records exposed through caching.
AuthenticationScope: Credential Exposure

Cached credentials or session data enables unauthorized access.

Example Code + Solution Code

Vulnerable Code

# VULNERABLE: Flask without cache control headers
from flask import Flask, render_template, jsonify

@app.route('/account/dashboard')
def dashboard_vulnerable():
    user = get_current_user()
    # Sensitive page can be cached by browser!
    return render_template('dashboard.html', user=user)

@app.route('/api/user/profile')
def profile_vulnerable():
    user = get_current_user()
    # API response with PII can be cached!
    return jsonify({
        'name': user.name,
        'ssn': user.ssn,
        'account_number': user.account_number
    })

# VULNERABLE: Sensitive data in response URL
@app.route('/receipt')
def receipt_vulnerable():
    # Sensitive data in URL - stored in history!
    return redirect(f'/receipt/view?ssn={ssn}&amount={amount}')
// VULNERABLE: Servlet without cache headers
@WebServlet("/account/details")
public class VulnerableServlet extends HttpServlet {

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        // No cache control headers!
        User user = getCurrentUser(request);

        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        out.println("<html><body>");
        out.println("<p>SSN: " + user.getSsn() + "</p>");
        out.println("<p>Account: " + user.getAccountNumber() + "</p>");
        out.println("</body></html>");
    }
}

// VULNERABLE: Spring controller
@Controller
public class VulnerableController {

    @GetMapping("/transactions")
    public String transactions(Model model) {
        // Response cached by default!
        model.addAttribute("transactions", getTransactions());
        return "transactions";
    }
}
<!-- VULNERABLE: HTML form without autocomplete control -->
<form action="/login" method="POST">
    <!-- Browser autocomplete enabled by default! -->
    <input type="text" name="username" placeholder="Username">
    <input type="password" name="password" placeholder="Password">

    <!-- Credit card can be saved! -->
    <input type="text" name="cardNumber" placeholder="Card Number">
    <input type="text" name="cvv" placeholder="CVV">

    <!-- SSN might be remembered! -->
    <input type="text" name="ssn" placeholder="Social Security Number">

    <button type="submit">Submit</button>
</form>
// VULNERABLE: Storing sensitive data in localStorage
function loginVulnerable(username, password) {
    // Credentials stored persistently!
    localStorage.setItem('username', username);
    localStorage.setItem('password', password);  // NEVER do this!
    localStorage.setItem('authToken', getToken());
}

// VULNERABLE: Sensitive data in sessionStorage
function loadUserProfile(user) {
    // PII in browser storage!
    sessionStorage.setItem('user', JSON.stringify({
        ssn: user.ssn,
        accountNumber: user.accountNumber,
        dateOfBirth: user.dob
    }));
}

// VULNERABLE: Not clearing data on logout
function logoutVulnerable() {
    // Just clears session, leaves cached data!
    fetch('/api/logout', { method: 'POST' });
    window.location = '/login';
    // localStorage/sessionStorage data persists!
}

// VULNERABLE: Express without cache headers
app.get('/api/sensitive-data', (req, res) => {
    // No cache control!
    res.json({
        ssn: req.user.ssn,
        accounts: req.user.accounts
    });
});

Fixed Code

# SAFE: Flask with cache control headers
from flask import Flask, render_template, jsonify, make_response

def add_no_cache_headers(response):
    """Add headers to prevent caching of sensitive data."""
    response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, private, max-age=0'
    response.headers['Pragma'] = 'no-cache'
    response.headers['Expires'] = '0'
    return response

@app.route('/account/dashboard')
def dashboard_safe():
    user = get_current_user()
    response = make_response(render_template('dashboard.html', user=user))
    return add_no_cache_headers(response)

@app.route('/api/user/profile')
def profile_safe():
    user = get_current_user()
    response = jsonify({
        'name': user.name,
        'ssn': user.ssn,
        'account_number': user.account_number
    })
    return add_no_cache_headers(response)

# SAFE: Apply to all sensitive routes
@app.after_request
def add_security_headers(response):
    """Add security headers to all responses."""
    # For authenticated pages
    if hasattr(g, 'current_user') and g.current_user:
        response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, private'
        response.headers['Pragma'] = 'no-cache'
        response.headers['Expires'] = '0'

    return response

# SAFE: POST for sensitive operations (not cached)
@app.route('/receipt', methods=['POST'])
def receipt_safe():
    # POST requests are not cached by browsers
    # Sensitive data in POST body, not URL
    ssn = request.form['ssn']
    amount = request.form['amount']
    return render_receipt(ssn, amount)
// SAFE: Servlet with cache headers
@WebServlet("/account/details")
public class SecureServlet extends HttpServlet {

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        // Set cache control headers
        response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, private");
        response.setHeader("Pragma", "no-cache");
        response.setDateHeader("Expires", 0);

        User user = getCurrentUser(request);

        response.setContentType("text/html");
        PrintWriter out = response.getWriter();
        out.println("<html><body>");
        out.println("<p>SSN: " + user.getSsn() + "</p>");
        out.println("<p>Account: " + user.getAccountNumber() + "</p>");
        out.println("</body></html>");
    }
}

// SAFE: Spring controller with cache control
@Controller
public class SecureController {

    @GetMapping("/transactions")
    public ResponseEntity<String> transactions() {
        return ResponseEntity.ok()
            .cacheControl(CacheControl.noStore())
            .header("Pragma", "no-cache")
            .header("Expires", "0")
            .body(renderTransactions());
    }
}

// SAFE: Spring Security configuration for cache control
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .headers(headers -> headers
                .cacheControl(cache -> {})  // Adds no-cache, no-store, etc.
                .contentTypeOptions(content -> {})
            );

        return http.build();
    }
}

// SAFE: Filter for all sensitive endpoints
@WebFilter("/secure/*")
public class NoCacheFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        HttpServletResponse response = (HttpServletResponse) res;

        response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, private");
        response.setHeader("Pragma", "no-cache");
        response.setDateHeader("Expires", 0);

        chain.doFilter(req, res);
    }
}
<!-- SAFE: HTML form with autocomplete controls -->
<form action="/login" method="POST" autocomplete="off">
    <!-- Specific autocomplete values -->
    <input type="text" name="username" autocomplete="username">

    <!-- Prevent password saving -->
    <input type="password" name="password" autocomplete="new-password">

    <!-- Financial fields - prevent autocomplete -->
    <input type="text" name="cardNumber" autocomplete="off"
           inputmode="numeric" pattern="[0-9]*">
    <input type="text" name="cvv" autocomplete="off"
           inputmode="numeric" maxlength="4">

    <!-- Sensitive PII - no autocomplete -->
    <input type="text" name="ssn" autocomplete="off"
           inputmode="numeric" pattern="[0-9-]*">

    <button type="submit">Submit</button>
</form>

<!-- For one-time codes -->
<input type="text" name="otp" autocomplete="one-time-code">
// SAFE: Secure client-side storage patterns
class SecureStorage {
    constructor() {
        // Use memory storage for sensitive data
        this.sensitiveData = new Map();
    }

    // Store sensitive data in memory only
    setSecure(key, value) {
        this.sensitiveData.set(key, value);
    }

    getSecure(key) {
        return this.sensitiveData.get(key);
    }

    // Clear all sensitive data
    clearAll() {
        this.sensitiveData.clear();
    }
}

const secureStorage = new SecureStorage();

// SAFE: Proper logout with data clearing
function logoutSafe() {
    // Clear all client-side data
    localStorage.clear();
    sessionStorage.clear();
    secureStorage.clearAll();

    // Clear cookies via server
    fetch('/api/logout', {
        method: 'POST',
        credentials: 'include'
    }).then(() => {
        // Force page reload to clear memory
        window.location.href = '/login';
    });
}

// SAFE: Clear sensitive data on page unload
window.addEventListener('beforeunload', () => {
    // Clear sensitive in-memory data
    secureStorage.clearAll();
});

// SAFE: Express with cache control
const helmet = require('helmet');

app.use(helmet.noSniff());

// Middleware for sensitive routes
const noCacheMiddleware = (req, res, next) => {
    res.set({
        'Cache-Control': 'no-store, no-cache, must-revalidate, private',
        'Pragma': 'no-cache',
        'Expires': '0',
        'Surrogate-Control': 'no-store'
    });
    next();
};

app.use('/api/sensitive', noCacheMiddleware);
app.use('/account', noCacheMiddleware);

app.get('/api/sensitive-data', noCacheMiddleware, (req, res) => {
    res.json({
        ssn: req.user.ssn,
        accounts: req.user.accounts
    });
});

// SAFE: Don't store sensitive data client-side
// Instead, use httpOnly cookies for session tokens
app.post('/login', (req, res) => {
    const token = generateToken(req.body.username);

    // Store token in httpOnly cookie - not accessible to JS
    res.cookie('session', token, {
        httpOnly: true,
        secure: true,
        sameSite: 'strict',
        maxAge: 3600000
    });

    // Don't return sensitive data to client
    res.json({ success: true });
});

// SAFE: Session-based data retrieval
app.get('/api/profile', requireAuth, (req, res) => {
    // Data fetched from server each time, not cached
    // User ID from secure session, not client storage
    const user = getUserFromSession(req);

    res.set('Cache-Control', 'no-store');
    res.json({
        name: user.name,
        // Only return what's needed
    });
});

Exploited in the Wild

Shared Computer Information Leakage

Public library and internet cafe computers have exposed previous users' banking and personal information through browser cache.

Browser History Exposure

Sensitive URLs containing tokens, IDs, or parameters exposed user data through browser history.

Form Autofill Attacks

Malicious websites have exploited browser autofill to steal credit card numbers and passwords from cached form data.


Tools to test/exploit

  • Browser Developer Tools — inspect cache, localStorage, sessionStorage.

  • Burp Suite — analyze cache headers.

  • OWASP ZAP — test for caching issues.

  • SecurityHeaders.com — check header configuration.


CVE Examples

  • CVE-2019-11358 — Cache-related information exposure.

  • CVE-2020-8945 — Sensitive data caching vulnerability.

  • Numerous application-specific caching issues.


References

  1. MITRE. "CWE-525: Use of Web Browser Cache Containing Sensitive Information." https://cwe.mitre.org/data/definitions/525.html

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