← Back to Blog

Fix "Cookie Without HttpOnly Flag" Vulnerability in Web Applications

Quick Fix for Panicking Engineers

PCI scan failing? Add HttpOnly to all cookies at the server level in 60 seconds:

# Apache (.htaccess or httpd.conf)
Header always edit Set-Cookie ^(.*)$ "$1; HttpOnly; Secure; SameSite=Lax"

# Nginx (in server or location block)
proxy_cookie_flags ~ httponly secure samesite=lax;

# PHP (php.ini, takes effect on next request)
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Lax

Reload your web server, then verify: curl -sI https://yourdomain.com | grep -i set-cookie

Every major vulnerability scanner flags this: Qualys reports it as QID 150122, Burp Suite marks it as "Cookie without HttpOnly flag set," and OWASP ZAP categorizes it under "Cookie No HttpOnly Flag" (Alert ID 10010). It is one of the most common findings in penetration tests and PCI ASV scans. The fix takes minutes, but getting it right across every cookie, every framework, and every reverse proxy layer requires precision. This guide covers the exact fix for every major platform.

What Scanners Report

Qualys WAS / PCI ASV

QID: 150122
Title: Cookie Without HttpOnly Flag Detected
Severity: 2 (Medium)
Category: Information Gathering
Impact: Session cookies accessible to client-side scripts, enabling session hijacking via XSS

CVSS v3.1: 5.3 (Medium)
OWASP: A05:2021 - Security Misconfiguration

Burp Suite Professional

Issue: Cookie without HttpOnly flag set
Severity: Low
Confidence: Certain
Path: /login
Cookie: PHPSESSID=abc123def456; path=/
Detail: The following cookie was issued without the HttpOnly flag:
  PHPSESSID=abc123def456

OWASP ZAP

Alert: Cookie No HttpOnly Flag
Risk: Low
Confidence: Medium
CWE ID: 1004 (Sensitive Cookie Without 'HttpOnly' Flag)
WASC ID: 13
Parameter: Set-Cookie: session_id

The severity rating varies by scanner, but the impact is consistent: without HttpOnly, any XSS vulnerability on your site allows an attacker to steal session cookies with document.cookie and hijack authenticated sessions.

Fix for Apache

Method 1: Header Module (Recommended)

# Enable mod_headers if not already loaded
sudo a2enmod headers
sudo systemctl restart apache2

# In your VirtualHost or .htaccess
# This rewrites ALL Set-Cookie headers to include HttpOnly and Secure
Header always edit Set-Cookie ^(.*)$ "$1; HttpOnly; Secure; SameSite=Lax"

Warning: This regex appends flags to every Set-Cookie header. If the application already sets HttpOnly, you will get duplicate flags (e.g., HttpOnly; HttpOnly). This is harmless per RFC 6265 but looks messy. Use this conditional version instead:

# Only add HttpOnly if not already present
Header always edit Set-Cookie "^((?!.*HttpOnly).*)$" "$1; HttpOnly"
Header always edit Set-Cookie "^((?!.*Secure).*)$" "$1; Secure"
Header always edit Set-Cookie "^((?!.*SameSite).*)$" "$1; SameSite=Lax"

Method 2: Apache mod_session (Session Cookies Only)

# In httpd.conf or VirtualHost
Session On
SessionCookieName session path=/;HttpOnly;Secure;SameSite=Lax

Fix for Nginx

Method 1: proxy_cookie_flags (Nginx 1.19.3+)

# In the server or location block that proxies to your backend
server {
    listen 443 ssl;
    server_name yourdomain.com;

    location / {
        proxy_pass http://backend;

        # Add HttpOnly and Secure to ALL cookies from the backend
        proxy_cookie_flags ~ httponly secure samesite=lax;
    }
}

The ~ is a wildcard matching all cookie names. To target a specific cookie:

# Only modify the PHPSESSID cookie
proxy_cookie_flags PHPSESSID httponly secure samesite=lax;

Method 2: proxy_cookie_path (Older Nginx)

# For Nginx versions before 1.19.3
# Use proxy_cookie_path to rewrite the Set-Cookie header
proxy_cookie_path / "/; HttpOnly; Secure; SameSite=Lax";

This works by rewriting the path=/ portion of the cookie. It is a hack, but it is reliable on older Nginx versions that lack proxy_cookie_flags.

Method 3: Header Manipulation with more_set_headers

# Requires the headers-more-nginx-module
# Rewrite Set-Cookie headers using Lua or more_set_headers
more_set_headers -s '200 302' 'Set-Cookie: $upstream_http_set_cookie; HttpOnly; Secure';

Fix for PHP

php.ini (Global)

; /etc/php/8.2/fpm/php.ini (or cli/php.ini for CLI)
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = Lax
session.cookie_lifetime = 0
session.use_strict_mode = 1
session.use_only_cookies = 1
# Reload PHP-FPM after changing php.ini
sudo systemctl reload php8.2-fpm

Application Code (Per-Cookie)

// PHP 7.3+ supports the options array
setcookie('user_pref', $value, [
    'expires'  => time() + 86400,
    'path'     => '/',
    'domain'   => '.yourdomain.com',
    'secure'   => true,
    'httponly'  => true,
    'samesite' => 'Lax',
]);

// For session cookies specifically
session_set_cookie_params([
    'lifetime' => 0,
    'path'     => '/',
    'domain'   => '.yourdomain.com',
    'secure'   => true,
    'httponly'  => true,
    'samesite' => 'Lax',
]);
session_start();

Fix for Node.js / Express

// Express session middleware
const session = require('express-session');

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,    // Prevents document.cookie access
    secure: true,      // Only sent over HTTPS
    sameSite: 'lax',   // CSRF protection
    maxAge: 3600000,   // 1 hour
    domain: '.yourdomain.com',
    path: '/',
  },
}));

// For individual cookies with res.cookie()
res.cookie('session_id', token, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  maxAge: 3600000,
});

// If using cookie-parser, set defaults
const cookieParser = require('cookie-parser');
app.use(cookieParser(process.env.COOKIE_SECRET));

Helmet.js (Recommended Companion)

const helmet = require('helmet');
app.use(helmet());
// Helmet does NOT set HttpOnly on cookies
// It handles other headers (CSP, HSTS, X-Frame-Options)
// You must still set httpOnly: true on your cookies explicitly

Fix for Django

# settings.py
SESSION_COOKIE_HTTPONLY = True   # Default is True in Django 1.0+
SESSION_COOKIE_SECURE = True     # Default is False, must enable
SESSION_COOKIE_SAMESITE = 'Lax'  # Default is 'Lax' in Django 2.1+
CSRF_COOKIE_HTTPONLY = True      # Default is False
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_SAMESITE = 'Lax'
LANGUAGE_COOKIE_HTTPONLY = True
LANGUAGE_COOKIE_SECURE = True
LANGUAGE_COOKIE_SAMESITE = 'Lax'

Important: CSRF_COOKIE_HTTPONLY = True will break Django's default JavaScript CSRF token handling. If your frontend reads the CSRF token from the cookie (the default pattern with jQuery and Axios), you need to instead read it from the {% csrf_token %} template tag or the X-CSRFToken meta tag. This is the correct approach anyway.

Fix for Ruby on Rails

# config/initializers/session_store.rb
Rails.application.config.session_store :cookie_store,
  key: '_myapp_session',
  httponly: true,
  secure: Rails.env.production?,
  same_site: :lax

# For individual cookies in controllers
cookies[:user_pref] = {
  value: 'dark_mode',
  httponly: true,
  secure: true,
  same_site: :lax,
  expires: 1.day.from_now,
}

# Force secure cookies in production
# config/environments/production.rb
config.force_ssl = true  # This also sets Secure flag on all cookies

Fix for WordPress

# wp-config.php (add before "That's all, stop editing!")

# Force Secure and HttpOnly on auth cookies
@ini_set('session.cookie_httponly', 1);
@ini_set('session.cookie_secure', 1);
@ini_set('session.cookie_samesite', 'Lax');

# WordPress-specific cookie settings
define('COOKIE_DOMAIN', '.yourdomain.com');
define('COOKIEPATH', '/');
define('ADMIN_COOKIE_PATH', '/wp-admin');

# Force HTTPS for admin and login
define('FORCE_SSL_ADMIN', true);

WordPress Plugin Cookies

Many WordPress plugins set their own cookies without HttpOnly. The server-level fix (Apache/Nginx header rewriting) catches these. If you need a code-level fix:

// functions.php or a must-use plugin
add_action('set_auth_cookie', function($auth_cookie, $expire, $expiration, $user_id, $scheme, $token) {
    // WordPress auth cookies are already HttpOnly in core
    // This hook is for monitoring/logging only
}, 10, 6);

// Fix third-party plugin cookies at the PHP level
add_action('send_headers', function() {
    $headers = headers_list();
    header_remove('Set-Cookie');
    foreach ($headers as $header) {
        if (stripos($header, 'Set-Cookie:') === 0) {
            if (stripos($header, 'HttpOnly') === false) {
                $header .= '; HttpOnly';
            }
            if (stripos($header, 'Secure') === false) {
                $header .= '; Secure';
            }
            header($header, false);
        }
    }
});

Audit Your Cookie Security

Use SecureBin's CSP Builder to generate a Content-Security-Policy header that complements your HttpOnly cookies with XSS protection.

Build CSP Header

Fix for Flask (Python)

# app.py or config.py
app.config.update(
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SECURE=True,
    SESSION_COOKIE_SAMESITE='Lax',
    REMEMBER_COOKIE_HTTPONLY=True,
    REMEMBER_COOKIE_SECURE=True,
)

# For individual cookies
from flask import make_response
resp = make_response(render_template('index.html'))
resp.set_cookie('user_pref', 'dark',
    httponly=True,
    secure=True,
    samesite='Lax',
    max_age=86400,
)

Fix for Spring Boot (Java)

# application.properties
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.same-site=Lax

# Or in application.yml
server:
  servlet:
    session:
      cookie:
        http-only: true
        secure: true
        same-site: Lax
// Programmatic configuration (Spring Security)
@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.sessionManagement(session -> session
            .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
        );
        return http.build();
    }

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setCookieName("JSESSIONID");
        serializer.setUseHttpOnlyCookie(true);
        serializer.setUseSecureCookie(true);
        serializer.setSameSite("Lax");
        return serializer;
    }
}

Verification

After applying the fix, verify with these commands:

curl Verification

# Check all Set-Cookie headers
curl -sI https://yourdomain.com/login -X POST \
  -d "user=test&pass=test" | grep -i 'set-cookie'

# Expected output (every cookie should have HttpOnly and Secure):
# Set-Cookie: PHPSESSID=abc123; path=/; HttpOnly; Secure; SameSite=Lax
# Set-Cookie: csrf_token=xyz789; path=/; HttpOnly; Secure; SameSite=Lax

# If you see a cookie WITHOUT HttpOnly, that specific cookie is not fixed:
# Set-Cookie: analytics_id=12345; path=/     <-- VULNERABLE

Browser DevTools Verification

# In Chrome DevTools > Application > Cookies:
# Every row should show a checkmark in the "HttpOnly" column
# Any row without a checkmark is still vulnerable

# In the Console tab, try accessing cookies:
document.cookie
# Should return ONLY non-HttpOnly cookies (analytics, preferences)
# Should NOT return session IDs, auth tokens, or CSRF tokens

Automated Verification with ZAP CLI

# Run OWASP ZAP in headless mode to re-scan
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \
  -t https://yourdomain.com -r /tmp/report.html

# Check specifically for cookie issues
docker run -t ghcr.io/zaproxy/zaproxy:stable zap-cli \
  quick-scan --self-contained --spider -r https://yourdomain.com \
  -l Low 2>&1 | grep -i 'httponly'

PCI DSS Compliance

PCI DSS v4.0 Requirement 6.2.4 states: "Software engineering techniques or other methods are defined and in use by software development personnel to prevent or mitigate common software attacks." Cookie security is explicitly called out in the testing procedures.

For PCI ASV scans (Qualys, Trustwave, Nessus), the missing HttpOnly flag is a medium severity finding that will cause scan failure if the affected cookie is a session identifier. Non-session cookies (analytics, preferences) flagged without HttpOnly are typically informational and will not fail the scan, but fixing all cookies removes the finding entirely.

Full Cookie Security Checklist for PCI

  • HttpOnly: Set on all session and authentication cookies
  • Secure: Set on all cookies (requires HTTPS)
  • SameSite: Set to Lax or Strict on all cookies
  • Path: Restrict to the narrowest path possible
  • Domain: Set explicitly (avoid overly broad domain scope)
  • Expires/Max-Age: Session cookies should not persist beyond the session
  • __Host- prefix: Consider using the __Host- prefix for session cookies (requires Secure, Path=/, no Domain)

Check Your SSL Configuration

HttpOnly cookies require HTTPS to be effective (the Secure flag prevents transmission over HTTP). Verify your SSL setup is correct.

Check SSL Certificate

Common Gotchas

CDN/Proxy Stripping Flags

Some CDNs and reverse proxies rewrite Set-Cookie headers during response processing. If you set HttpOnly at the application level but the scanner still reports it missing, check whether an intermediate proxy is stripping the flag. Test by curling the origin directly:

# Bypass CDN, hit origin directly
curl -sI -H "Host: yourdomain.com" https://origin-ip/login | grep -i set-cookie

# Compare with CDN response
curl -sI https://yourdomain.com/login | grep -i set-cookie

Multiple Set-Cookie Lines

HTTP allows multiple Set-Cookie headers in a single response. Your fix must handle all of them. A common mistake is fixing only the first Set-Cookie header while leaving subsequent ones vulnerable.

JavaScript Frameworks Setting Cookies

Client-side JavaScript can set cookies via document.cookie. These cookies cannot have the HttpOnly flag because HttpOnly is a server-side attribute. If your JavaScript framework sets cookies, either move cookie creation to the server side or accept that those specific cookies cannot be HttpOnly (they should not contain sensitive data).

Summary

The "Cookie Without HttpOnly Flag" finding is one of the easiest security vulnerabilities to fix and one of the most common to overlook. For an immediate fix, add HttpOnly at the reverse proxy level (Apache Header edit or Nginx proxy_cookie_flags) to catch all cookies regardless of which backend application sets them. Then fix it at the application level in each framework's session configuration. Verify with curl -sI and re-run your scanner. For PCI compliance, ensure all session cookies also have Secure and SameSite flags. The entire fix, from diagnosis to verification, should take under 15 minutes per server.

Related Articles

Continue reading: Fix Incomplete SSL Certificate Chain, Fix CORS No Access-Control-Allow-Origin, Fix Nginx 502 Bad Gateway with PHP-FPM.

UK
Written by Usman Khan
DevOps Engineer | MSc Cybersecurity | CEH | AWS Solutions Architect

Usman has 10+ years of experience securing enterprise infrastructure, managing high-traffic servers, and building zero-knowledge security tools. Read more about the author.