By Anders Vik ·

HttpOnly Cookie Flag Explained: What It Blocks and Misses

You ran a scan on your own site, or someone else ran it for you, and the report came back with a line reading missing_httponly next to your session cookie. Maybe missing_secure sits right below it. Before you patch anything, it helps to know what you're actually fixing. An HttpOnly cookie is a cookie the server marks so that JavaScript in the browser cannot read or write it, which matters because a session cookie is, in practice, a bearer token: whoever holds it is logged in as you, no password required.

I spent years running shared-hosting fleets where this exact finding showed up on customer tickets every week, usually attached to a pentest report the customer didn't have time to read twice. So let's do this properly: what each flag stops, what it doesn't, and the server config that fixes it without doubling flags or breaking your login flow. To see where your own site stands first, run the URL through cookiecheck.tools and it will list every cookie your server sets along with its flags.

Understanding HttpOnly and Secure Cookie Flags

What the HttpOnly flag actually stops

HttpOnly is a cookie attribute, set as part of the Set-Cookie response header, that tells the browser to keep that cookie out of reach of document.cookie and any other JavaScript API. Per the current draft of the cookie standard, a script attempting to read or write an HttpOnly cookie is simply ignored, as if the cookie weren't there. The browser still sends the cookie automatically with ordinary requests, including ones triggered by fetch() or XMLHttpRequest. It just won't hand the value to any script that asks for it directly.

That distinction is the whole point. If an attacker manages to run JavaScript on your page through a cross-site scripting (XSS) flaw, the classic move is to read document.cookie and ship the session value off to a server they control. An HttpOnly cookie can't be read that way, so that exfiltration path is closed. PortSwigger's XSS training material lists HttpOnly among the practical limitations that make stolen cookies less useful, which pushes attackers toward other targets, like auto-filled passwords or CSRF tokens sitting in the page.

Here's what HttpOnly does not do, and this is the part every two-paragraph blog post skips:

  • It does not stop the XSS vulnerability itself. The injected script still runs. It just can't read this one cookie.
  • It does not stop an XSS-driven CSRF attack, because the browser attaches the cookie to any request the malicious script fires, HttpOnly or not.
  • It does not fully protect against integrity attacks. Browsers cap the number of cookies per domain to a few hundred. Flood the jar with junk and older cookies, including HttpOnly ones, get evicted. A script can then set a new cookie with the same name, no HttpOnly flag, and your server may read that instead. This is called cookie jar overflow, documented by researcher Sjoerd Langkemper in 2020.
  • It does nothing against malware on the user's own device. Google's Chromium team put it plainly in a 2024 post on cookie theft: stolen session cookies "continue to work even after the malware is detected and removed," because the malware operates at the same privilege level as the browser itself.

None of that makes HttpOnly optional. It closes a real, commonly exploited path at close to zero cost. It just isn't a substitute for fixing the XSS hole in the first place.

Can JavaScript read an HttpOnly cookie?

No. That's the short answer, and it's worth being direct about it because the longer answer is where people get tripped up. MDN's Set-Cookie reference states it cleanly: HttpOnly "forbids JavaScript from accessing the cookie," whether through document.cookie, the Cookie Store API, or anything else that touches script-level cookie access.

What confuses people is a different symptom. You add HttpOnly to a session cookie, deploy, and your fetch() calls start coming back 401. It looks like HttpOnly broke the request, so the instinct is to remove it.

Nine times out of ten, that's not what happened. The browser still attaches an HttpOnly cookie to a same-origin fetch or XHR request automatically, exactly like it always did. What usually broke is that the fetch call is missing credentials: 'include' (or credentials: 'same-origin'), the setting that tells the browser to attach cookies to that request at all. Check that before you touch the flag.

What the Secure cookie flag actually stops

The cookie secure flag is a separate attribute with a separate job: it restricts the cookie to connections made over HTTPS. MDN's phrasing is precise here too, the cookie is "only sent when a request is made with the https: scheme," with one carve-out for localhost, which I'll come back to.

The http cookie secure flag rule has real teeth in modern browsers, and the current draft of the cookie standard formalizes behavior that used to be browser folklore. Two rules matter most:

  • A cookie carrying the secure cookie attribute that arrives over a plain HTTP connection is rejected outright. Browsers began enforcing this back in 2016, and it's now written into the spec's storage algorithm. If a Secure cookie gets set on an http:// hop, it never lands: no console error, no warning, and your login breaks with no obvious cause.
  • An insecure cookie cannot overwrite an existing Secure cookie of the same name and domain. This closes an older integrity hole where a plain HTTP page on the same domain could clobber a cookie that was supposed to be HTTPS-only.

What Secure does not do: it says nothing about where the cookie ends up once it's received. A compromised TLS endpoint, a reverse proxy that logs headers in plaintext, or malware on the user's machine are all outside its scope. Secure protects the wire between the browser and your server, nothing more.

One practical wrinkle: Chrome and Firefox treat http://localhost as a secure context for cookie purposes, so Secure cookies set fine there even without TLS. Safari does not. If a cookie "works fine on my machine" but vanishes on a staging server still running plain HTTP, this mismatch is almost always why.

Pairing both flags with the __Host- prefix

HttpOnly and Secure solve different problems, so a session cookie generally wants both. The end state looks like this:

Set-Cookie: __Host-session=abc123; Path=/; Secure; HttpOnly; SameSite=Lax

The __Host- prefix in the name is a signal the browser itself enforces: a cookie named with that prefix must carry Secure, must not carry a Domain attribute (making it host-only, unreachable by subdomains), and must set Path=/. Get any of those wrong and the browser refuses to store the cookie at all. OWASP recommends the __Host- prefix specifically for session identifiers because it removes an entire class of misconfiguration (scoping the session cookie to a wildcard domain) at the browser level.

One flag worth a single sentence here: if you ever set SameSite=None on a cookie, meaning you want it sent on cross-site requests as a third-party cookie, the specification requires Secure alongside it, no exceptions. The full mechanics of SameSite and how it relates to CSRF deserve their own treatment rather than a rushed paragraph here.

How common are these flags in the wild

If your scan came back flagged, you're not an outlier. The HTTP Archive's Web Almanac 2024 security chapter, drawn from its desktop crawl, found HttpOnly present on 42% of first-party cookies and Secure present on 44%, both up roughly six to seven points since the 2022 crawl but still short of a majority. The __Host- prefix, despite the protection it buys for almost no effort, showed up on just 0.17% of first-party cookies. The Almanac's authors called that gap "particularly surprising."

So missing flags are common. That doesn't make them fine to leave alone, especially on anything that authenticates a user, but you're fixing a widespread gap, not a one-off misconfiguration unique to your stack.

Setting HttpOnly and Secure at the server

The cleanest fix is always in the application code that issues the cookie, since that's where you control which cookies get which flags. But plenty of sites run software they don't want to patch, or want a safety net that catches cookies the app forgets. That's what the server-level fix is for.

Apache: editing Set-Cookie with mod_headers

This goes in your virtual host config or an .htaccess file, and it needs mod_headers enabled (a2enmod headers on Debian and Ubuntu, or check your host's module list on shared hosting).

Header always edit Set-Cookie ^((?i:(?!.*;\s*Secure).*))$ "$1; Secure"
Header always edit Set-Cookie ^((?i:(?!.*;\s*HttpOnly).*))$ "$1; HttpOnly"

Two directives, one job each. The first line matches any Set-Cookie header that doesn't already contain ; Secure (case-insensitive, via the (?i:...) inline flag) and appends it. The second line does the same for HttpOnly. Two separate negative-lookahead checks matter because a lot of application frameworks already set one flag but not the other.

A blunter version you'll see on forums, Header always edit Set-Cookie (.*) "$1; HttpOnly; Secure", works but will double up flags on any cookie the app already secured. Browsers tolerate the duplicate silently, but it's untidy, it can confuse a scanner into a false positive, and it tends to turn into a confused GitHub issue six months later.

The word always in each directive matters too. Without it, Header edit only touches headers on responses in the normal 2xx processing path. With always, it also applies to redirects and error documents, which is exactly where a lot of login cookies get set (a POST to a login handler that immediately redirects to the dashboard).

If a script on your page legitimately needs to read a specific cookie, say a CSRF token cookie your frontend framework reads before submitting a form, exclude it by name:

Header always edit Set-Cookie ^((?!XSRF-TOKEN).*)$ "$1; HttpOnly"

Test both regexes against a real response before you rely on them in production. Confirm with curl -I against your own vhost rather than trusting a snippet from any article, including this one.

nginx: proxy_cookie_flags

nginx doesn't have a direct equivalent to Apache's Header edit for cookies, but if you're running nginx as a reverse proxy in front of an application server, proxy_cookie_flags (available since nginx 1.19.3) does the job for anything passing through proxy_pass:

proxy_cookie_flags XSRF-TOKEN secure;
proxy_cookie_flags ~ secure httponly;

Order matters here, and it's the opposite of what feels natural: the first matching directive wins, so the specific exception (the cookie name you don't want HttpOnly on) goes above the catch-all regex (~ matches everything). Put the catch-all first and the exception never fires. This directive only rewrites cookies from an upstream reached through proxy_pass; if PHP-FPM talks to nginx over FastCGI directly, proxy_cookie_flags won't see those cookies at all. In that setup, fix the flags in PHP itself.

PHP: setting the flags at the source

If your sessions run through PHP, the ini settings are the most direct fix, since they apply the flags at the point the cookie is created rather than rewriting it after the fact:

session.cookie_secure = 1
session.cookie_httponly = 1
session.use_only_cookies = 1
session.cookie_samesite = "Lax"

Set these in php.ini or a per-directory .user.ini on shared hosting. session.cookie_secure and session.cookie_httponly do exactly what their names say. session.use_only_cookies stops PHP from passing the session ID in the URL, which would leak it into browser history and server logs regardless of what flags the cookie carries. Setting the flags in PHP is the primary fix, and a server-level rewrite rule is the safety net that catches anything the app forgets, including third-party libraries that set their own cookies.

Two ways this breaks a site

I've seen both of these take down a login flow, so a quick warning on each.

Adding Secure while the site is still reachable over plain HTTP on any path. Once you push the config, any cookie set during that HTTP request just doesn't arrive, and the user gets logged out or can't log in at all, with nothing in the console to explain why. Redirect all HTTP traffic to HTTPS, and consider adding HSTS, before or alongside adding Secure. The .htaccess rule generator at htaccess.tools can build that HTTP-to-HTTPS redirect block if you'd rather not hand-write the regex.

Adding HttpOnly to a cookie a script actually needs to read. A CSRF token cookie, a locale preference, an A/B test flag, anything your frontend JavaScript pulls out of document.cookie will start returning empty strings the moment HttpOnly lands on it, and whatever depended on that value breaks quietly. The exclusion regex shown above for Apache, or the ordered directives for nginx, solves this. The fix isn't to skip HttpOnly everywhere, it's to name the exception explicitly.

Verify it worked

Once you've deployed the config change, check it two ways. The fast one is a single curl request against the page that sets the cookie:

curl -I https://example.com/login | grep -i set-cookie

You're looking for ; Secure and ; HttpOnly on the cookie line, with each flag appearing once. The more complete check is to run the same URL back through cookiecheck.tools, which will show every cookie your server sets and clear the missing_secure, missing_httponly, secure_on_http, or bad_host_prefix codes once the fix has actually landed. If you're unsure what the report is scoring, the about page breaks down what each section of the score measures.

Frequently Asked Questions

What does the HttpOnly flag do?

HttpOnly is a Set-Cookie attribute that blocks JavaScript from reading or writing that cookie through document.cookie or similar script-level APIs. The browser still sends the cookie automatically with normal requests, including ones triggered by fetch or XHR, it just refuses to expose the value to any script on the page. That closes the most common way an XSS vulnerability turns into stolen session cookies, though it doesn't fix the XSS hole itself.

What does the Secure cookie flag do?

The Secure attribute restricts a cookie to HTTPS connections. Browsers reject a Secure cookie outright if it's received over plain HTTP, and an insecure connection can't overwrite an existing Secure cookie of the same name. In practice this stops the cookie from ever traveling in plaintext. One exception worth knowing: most browsers treat localhost as secure for testing purposes even without TLS.

Can JavaScript read an HttpOnly cookie?

No. Per the cookie specification and MDN's Set-Cookie documentation, any attempt to access an HttpOnly cookie through document.cookie or a similar script API is simply ignored by the browser. If your JavaScript stops receiving a cookie's value after you add HttpOnly, that is expected. If an entire fetch request starts failing with a 401, the more common culprit is a missing credentials: 'include' setting on the request, not the flag itself.

How do I set HttpOnly and Secure on Apache or nginx?

On Apache, use mod_headers with Header always edit Set-Cookie and a regex that only appends the flag if it isn't already present. On nginx, use proxy_cookie_flags (since version 1.19.3) for cookies passing through a proxied upstream, putting any named exceptions above the catch-all regex since the first matching directive wins. If your app runs on PHP, setting session.cookie_secure and session.cookie_httponly to 1 in php.ini fixes the session cookie at the source.