Remediation Guides

Cookies Not Marked Secure: How to Set the Secure Flag on Session Cookies

26 September 2026 8 min read

“Cookies not marked secure” means the web application sends a Set-Cookie header without the Secure attribute, so browsers also send it over plain HTTP, where anyone on the network path can read it. Fix it by adding Secure to every session and sensitive cookie, in the application or at the reverse proxy, and serving the site only over HTTPS.

What the scanner is actually detecting

Every tool below inspects the Set-Cookie response headers it receives and flags cookies that lack the Secure attribute. None of them exploits anything; they are reading headers.

Scanner Finding title ID Severity
Nessus Web Application Cookies Not Marked Secure Plugin 85602 Info, family Web Servers
Tenable Web App Scanning Cookie Without Secure Flag Detected Plugin 98064 Low (CVSS v3 3.1)
OWASP ZAP Cookie Without Secure Flag Alert 10011 (passive) Low
Burp Suite TLS cookie without secure flag set n/a Medium

The scope differs by tool. Tenable describes plugin 85602 as covering every cookie seen during authenticated and unauthenticated crawling, not only session cookies, and it also reports applications served over unencrypted HTTP. Nessus has a separate, session-cookie-only check titled “Web Application Session Cookies Not Marked Secure”. ZAP’s rule is public: it runs only on HTTPS responses, skips cookies that are already expired (deletion cookies), and ignores names on its configured cookie ignore list. Burp reports cookies set during HTTPS sessions that it judges sensitive. All four map to CWE-614.

Real-world risk

Without Secure, the browser attaches the cookie to any request for that host over http://: an old bookmark, a hardcoded link, a redirect from port 80, or a request an attacker triggers by injecting an http:// image tag into another page the victim loads. Someone on the network path (hostile Wi-Fi, a compromised router, ARP spoofing on a LAN) can then capture a session token and reuse it. Closing port 80 does not remove this, because an on-path attacker can answer that connection themselves.

Keep the limits in view. The attack needs a network position and applies mainly to cookies that authenticate or carry sensitive data; a language preference cookie without Secure is close to meaningless. RFC 6265 also notes that Secure protects only confidentiality: an active attacker can still overwrite secure cookies from an insecure channel. That is why Nessus rates this Info while Burp, which filters for sensitive cookies, rates it higher.

How to confirm it on the host

Use a GET request, since some applications set no cookies on HEAD, and check each Set-Cookie line for Secure:

curl -sk -D - -o /dev/null https://www.example.com/ | grep -i '^set-cookie'
curl -sk -D - -o /dev/null https://www.example.com/login | grep -i '^set-cookie'

On Windows:

$r = Invoke-WebRequest -Uri https://www.example.com/login -UseBasicParsing
$r.Headers['Set-Cookie']

Cookies issued after login are the ones that matter most. Log in with a browser and open the developer tools (Application panel in Chrome and Edge, Storage panel in Firefox); the cookie list has a Secure column. Then find where the cookie is created:

php -i | grep -E 'session.cookie_(secure|httponly)'
sudo nginx -T | grep -i proxy_cookie_flags
sudo grep -Ri 'edit Set-Cookie' /etc/apache2/ /etc/httpd/ 2>/dev/null
findstr /s /i "requireSSL" C:inetpubwwwrootweb.config

The PHP CLI can load a different php.ini than PHP-FPM, so confirm with phpinfo() served by the real site if the CLI output looks right but the header does not.

How to fix it

First make sure the site is reachable over HTTPS and that port 80 only redirects. Per MDN, pages served over http: cannot set cookies with the Secure attribute, so an HTTP-only application has to move to HTTPS before this fix is possible.

PHP

For the native session cookie, set these in the php.ini (or pool configuration) your web SAPI loads, then restart PHP-FPM or the web server. The unit name varies, for example php8.3-fpm on Debian and Ubuntu or php-fpm on RHEL-family systems.

session.cookie_secure = 1
session.cookie_httponly = 1

Or set it in code before session_start():

session_set_cookie_params(['secure' => true, 'httponly' => true, 'samesite' => 'Lax']);
session_start();

These settings affect only the session cookie. Cookies the application creates with setcookie() need ‘secure’ => true in their options array, and frameworks with their own session layer have their own setting.

ASP.NET on IIS (.NET Framework)

<system.web>
  <httpCookies requireSSL="true" httpOnlyCookies="true" />
  <authentication mode="Forms">
    <forms requireSSL="true" />
  </authentication>
</system.web>

Microsoft documents that httpCookies requireSSL is overridden by any feature exposing its own requireSSL setting, such as the forms element, so set both when Forms authentication is in use. Merge the forms attribute into your existing element rather than replacing it.

ASP.NET Core

app.UseCookiePolicy(new CookiePolicyOptions
{
    Secure = CookieSecurePolicy.Always
});

CookieSecurePolicy.SameAsRequest marks cookies Secure only when the request arrived over HTTPS. Behind a TLS-terminating proxy the application sees HTTP, so cookies come out without the flag unless you use Always or forward the original scheme to the app.

Java on Tomcat

In the application’s web.xml (Servlet 3.0 and later):

<session-config>
  <cookie-config>
    <http-only>true</http-only>
    <secure>true</secure>
  </cookie-config>
</session-config>

Tomcat already marks JSESSIONID Secure when request.isSecure() is true, which is why the finding usually appears only behind a load balancer that offloads TLS. For a connector that receives only proxied HTTPS traffic, the Tomcat documentation’s secure and scheme attributes fix that at the source:

<Connector port="8080" protocol="HTTP/1.1" secure="true" scheme="https"
           proxyName="www.example.com" proxyPort="443" />

Apache HTTP Server as reverse proxy

With mod_headers enabled, rewrite every Set-Cookie header in the HTTPS virtual host:

Header edit Set-Cookie "^(.*)$" "$1; Secure"
Header always edit Set-Cookie "^(.*)$" "$1; Secure"

Both lines are deliberate. The Apache documentation explains that always is not a superset of the default onsuccess table, and headers from CGI scripts or mod_proxy_fcgi (PHP-FPM) live in the always table. If a cookie already carries Secure, the duplicate attribute is harmless under RFC 6265 parsing. Test with sudo apachectl configtest, then reload.

nginx 1.19.3 and later as reverse proxy

location / {
    proxy_pass http://127.0.0.1:8080;
    proxy_cookie_flags ~ secure;
}

The ~ is a regular expression that matches every cookie name. When several proxy_cookie_flags lines match a cookie, nginx applies only the first one, so a more specific line placed above it will stop the catch-all from applying to that cookie. The directive belongs to the proxy module and rewrites only proxy_pass responses; for PHP-FPM behind fastcgi_pass, fix it in php.ini. Apply with sudo nginx -t && sudo systemctl reload nginx.

How to verify the fix and rescan

Repeat the curl checks and the browser check after a full login, logout and password reset, since some cookies appear only in those flows. Every Set-Cookie over HTTPS should end in Secure. Then rescan with the same scan policy and the same credentials: plugin 85602 evaluates authenticated pages, so a rescan without credentials can look clean while logged-in cookies are still wrong. Scan the same hostname or IP the original finding used, because the origin server and the load balancer in front of it can return different headers.

What can break and how to roll back

  • Any HTTP path that needs the cookie. Browsers stop sending it over HTTP, so mixed HTTP and HTTPS applications, HTTP health checks that log in, or internal links hardcoded to http:// will lose the session and often loop back to the login page.
  • Local development. Microsoft notes that with CookieSecurePolicy.Always, local development also needs HTTPS URLs.
  • Client-side scripts. Secure alone does not hide cookies from JavaScript, but if you add httponly in the same change, frontends that read a cookie (for example a CSRF token) will break. Change one attribute at a time.

Rollback is a single configuration change: remove the directive or set the value back, then reload the service. Cookies already stored with Secure keep it until they expire or are reissued, so users may need to sign in again.

Common false positive reasons

  • Non-sensitive cookies. Tenable’s own solution text asks you to review each cookie; analytics or language cookies are technically true findings with little risk. Fixing them is usually cheaper than documenting exceptions.
  • Deletion cookies. A logout response that expires a cookie may be flagged by tools that, unlike ZAP, do not skip expired cookies.
  • Cookies added by infrastructure. Load balancers and WAFs can insert their own cookies, so the application is fine but the device is not; see how a WAF changes what your web scanner reports.
  • Scanning the wrong layer. The proxy adds Secure, but the scanner hit the backend directly over HTTP.

This is a runtime behavior of the deployed stack, which is why dynamic testing finds it and source review often does not; the differences between SAST, DAST and SCA explain that gap.

FAQ

If port 80 is closed, do I still need the Secure flag?

Yes. An on-path attacker can impersonate the HTTP endpoint and receive the cookie whether or not your server listens there.

Does HttpOnly fix this finding?

No. HttpOnly blocks JavaScript access; Secure controls which channel carries the cookie. Scanners report them separately.

Does HSTS make the Secure flag unnecessary?

No. HSTS does not cover a browser’s first visit (unless preloaded) or hosts outside its scope. Use both.

Why is it Info in Nessus but Medium in Burp?

Plugin 85602 reports every cookie without triage, while Burp reports cookies it considers sensitive. Rate each cookie by what it grants.

Tracking this finding across many hosts

Cookie findings tend to arrive from several tools at once. SITEY, a self-hosted vulnerability management platform, imports findings from 16 scanners, but duplicates are merged per scanner, not across scanners, so the same cookie reported by Nessus and Burp stays as two records. Per-finding retest works for Nessus, Acunetix and Burp findings; for ZAP or Tenable WAS, rerun the scan to confirm closure.

Sources

SITEY closes the loop, not just the report.Discover, validate, fix and verify in your own infrastructure.

See pricing