How this check works
Scanaris looks at your page's main response and checks two conditions at once. First: the response sets at least one cookie (Set-Cookie) whose name starts with sess, sid, auth, token, jwt, connect.sid, phpsessid or asp.net, that is, a cookie that in all likelihood identifies a session. Second: the same response carries Cache-Control with the public directive. Only if both hold does it open a medium-severity finding with the cookie name and the header value. It is deliberately strict: a preferences cookie with public caching, or a session cookie without public, is not flagged.
Why it matters
Cache-Control: public is an explicit instruction to any shared cache (a CDN, a reverse proxy such as Varnish or nginx, a company's or carrier's proxy) that the response may be stored and handed to other users. If that same response includes a session Set-Cookie, the cache may store the cookie along with the body and give it to the next visitor. That visitor walks in with the previous user's session, no password needed, and sees their data. It is the kind of flaw discovered when a customer writes to say they "got into someone else's account". It has happened at banks and large retailers, always from the same combination: personalised content marked as cacheable.
Why it happens
Usually from an over-broad cache rule: an add_header Cache-Control "public, max-age=3600" in nginx applied to every route, including those that create sessions; a "Cache Everything" rule in Cloudflare; or a routeRules entry with cache headers for '/**' in an app that sets cookies on the main page. Most CDNs refuse to store responses with Set-Cookie by default, and nginx does too unless you tell it to ignore that header, but an explicit public is you telling them to do it.
How to fix it
- Responses that create or renew a session carry
Cache-Control: private, no-store. That is whatexpress-session, Laravel, Django and Rails do by default; if you seepublic, a later layer added it. - Separate cache rules by route type.
publicwith a longmax-agefor static files with a hash in the name (/_nuxt/,/_next/static/,/assets/);privateorno-storefor HTML that depends on the user. In nginx, use separatelocationblocks; in Nuxt and Next.js, per-route rules instead of a global one. - Do not set the session cookie on public cacheable pages. If the home page does not need a session, it should not create one: create the cookie at login or when entering the dashboard.
- If you use a CDN with "Cache Everything" or similar, add a rule not to cache responses with
Set-Cookieor authenticated routes. And addVary: Cookieas an extra net, although it does not replace the above.
Check it is fixed
curl -sI https://yourdomain.com | grep -iE "^(set-cookie|cache-control|vary):"
curl -sI https://yourdomain.com/dashboard | grep -iE "^(set-cookie|cache-control):"
If there is a Set-Cookie with a session name, that response's Cache-Control must be private, no-store or no-cache, never public. Make the second request with and without your session cookie: the authenticated response must never appear without it.
Frequently asked questions
My CDN says it does not cache responses with cookies. So is it a false positive?
The leak is probably not happening today, but you depend on a default behaviour that a new rule or a change of provider can switch off. The header says it is public; fix the header and stop depending on the exception.
What about analytics or consent cookies?
They are not flagged: only names that indicate a session count. A consent cookie on a public cached page is normal. How session cookies should be configured (Secure, HttpOnly, SameSite) is covered by insecure cookies.
My session is a token in localStorage, not a cookie.
Then this check does not apply, but token in storage does: a token reachable from JavaScript has other problems.