Cookies and Sessions over HTTP

HTTP is stateless: every request stands alone. A cookie is how a server asks the browser to remember a value: the response carries Set-Cookie: sid=k3Vq9XbT2mR7wLp4; Path=/; Secure; HttpOnly, and from then on the browser adds Cookie: sid=k3Vq9XbT2mR7wLp4 to every matching request. A session is the server's half: a record of who is logged in, kept on the server and found through that identifier.

When you open your order history after logging in to a shop, the shop knows it is you only because the browser sent the session cookie and the server found a live session for it. Without a live session, the request gets the login redirect or the 401 response you met in HTTP status codes.

Why it matters

Almost every feature behind a login depends on this mechanism, and many of its defects are security defects: a session identifier that scripts can read, that travels over plain HTTP, that still works after logout or that never expires lets someone else act as the user. None of this shows in a happy-path run.

The checks are cheap and concrete: developer tools and a test account show whether an attribute is present and whether a replayed request is refused, and a report quoting the exact Set-Cookie line lets a developer fix the configuration quickly.

Scope and lifetime: Domain, Path, Expires and Max-Age

A server sets one cookie per Set-Cookie header: name=value, then attributes. RFC 6265 defines how the browser stores it and which requests receive it. The browser returns only the name=value pairs, in one Cookie header, so check attributes in the response, not in the request.

  • Domain: without it the cookie is host-only and returns only to the host that set it; Domain=portal.example also sends it to every subdomain, such as docs.portal.example.
  • Path: the cookie goes to URLs whose path starts with the cookie path at a / boundary, so Path=/help matches /help/start but not /helpdesk. RFC 6265 warns that Path is not a security boundary.
  • Expires (a date) or Max-Age (seconds) makes the cookie persistent, and Max-Age wins if both are set. Without either it is a session cookie, dropped when the browser session ends. Max-Age=0 deletes it.
  • A cookie is identified by name, domain and path together. A delete or overwrite must repeat all three; otherwise the old cookie stays, and an overwrite only adds a second cookie with the same name.
Responses from https://portal.example
Set-Cookie: sid=k3Vq9XbT2mR7wLp4; Path=/; Secure; HttpOnly; SameSite=Lax
Set-Cookie: lang=ar; Domain=portal.example; Path=/; Max-Age=31536000; Secure
Set-Cookie: tour=done; Path=/help; Expires=Fri, 01 Jan 2027 00:00:00 GMT; Secure

Request to                               Cookies the browser sends
https://portal.example/help/start        sid, lang, tour
https://portal.example/dashboard         sid, lang
https://docs.portal.example/             lang

Try it: Cookie Analyzer Try it: URL Parser

Security attributes: Secure, HttpOnly and SameSite

These attributes limit how a cookie can leak or be misused. Each one blocks one path, so know what it does not block.

  • Secure: the cookie is sent only over HTTPS, so it cannot be read from plain HTTP traffic.
  • HttpOnly: the cookie is hidden from document.cookie, so an injected script cannot copy it, but the browser still attaches it to requests, so such a script can still act as the user inside the page.
  • SameSite controls requests that start on another site. Strict: never sent, so a user arriving from another site's link looks logged out. Lax: sent only on top-level navigations, where the whole tab moves to the site (for example by following a link), with a safe method such as GET; not on cross-site form posts, images, frames or fetch calls. None: sent on cross-site requests too, unless browser settings block third-party cookies, and accepted only with Secure.
  • A "site" is the registrable domain, so docs.portal.example is the same site as portal.example. SameSite comes from the RFC 6265bis drafts that browsers implement, not from RFC 6265, and defaults differ between browsers, so expect it to be set explicitly.
  • Baseline for a session cookie: Secure, HttpOnly, an explicit SameSite=Lax or Strict, no Domain unless subdomains need it, and no Expires or Max-Age, because the OWASP Session Management Cheat Sheet recommends non-persistent session cookies.

Try it: Cookie Analyzer Try it: HTTP Headers Parser

Server-side sessions: identifier, expiry and logout

At login the server keeps a session record (user, roles, times) in a session store such as memory, a database or a cache, and sends only a random identifier in Set-Cookie. Each later request is looked up by that identifier: a live session runs it as that user; otherwise a page is redirected to the login page and an API returns 401 Unauthorized.

So there are two clocks, the cookie's in the browser and the session's on the server, and only the server's protects, because whoever holds the identifier can keep sending it. The OWASP Session Management Cheat Sheet calls for:

  • An identifier from a cryptographically secure random generator, with at least 64 bits of entropy (a measure of how hard it is to guess) and no meaning such as a user id or a timestamp.
  • A new identifier at login and at every privilege change, with the old one invalidated; otherwise an identifier planted before login (session fixation) still works after it.
  • An idle timeout counted from the last request and an absolute timeout counted from login, both enforced on the server.
  • A logout that ends the record on the server; deleting the cookie with Max-Age=0 only tidies the browser.
  • The identifier only in the cookie, never in the URL, where it leaks into history, logs and Referer headers.
Login stores the session on the server and sends only its random id in Set-Cookie; logout must end that record, so the tester's replay of the old id is sent to the login page.
  • Browser test account
  • Web server portal.example
  • Session store memory, database or cache
  1. 1 From Browser To Web server: Log in POST /login
  2. 2 From Web server To Session store: Create a session with a new random id the pre-login id stops working
  3. 3 From Web server To Browser: Only the id goes to the browser reply Set-Cookie: sid=k3Vq…; Secure; HttpOnly
  4. 4 From Browser To Web server: Every matching request carries it Cookie: sid=k3Vq…
  5. 5 From Web server To Session store: Look up the id: live session the idle timer restarts
  6. 6 From Web server To Browser: This user's dashboard reply 200 OK
  7. 7 From Browser To Web server: Log out POST /logout
  8. 8 From Web server To Session store: End the session record the step that actually logs the user out
  9. 9 From Web server To Browser: Delete the cookie reply Set-Cookie: sid=; Path=/; Max-Age=0
  10. 10 From Browser To Web server: Tester replays the old id Cookie: sid=k3Vq…
  11. 11 From Web server To Session store: Look up the id: no session
  12. 12 From Web server To Browser: Login page, never dashboard data reply 302 Location: /login Pass

Outcome: The cookie is only the key: logout and timeouts protect the user only when the server ends the record.

Worked example: tracing and testing a login session

A staff portal in a test environment, https://portal.example, requires: "A session ends after 15 minutes without activity and 8 hours after login. Log out ends the session." With a test account and the Network panel open, the tester logs in, opens the dashboard, logs out and records the exchange below.

Test only systems you own or are allowed to test, and mask session identifiers in reports, since a copied live identifier works like a password. Each check becomes a test case:

  • Attributes: sid has Secure, HttpOnly and SameSite=Lax, and no Domain or Max-Age.
  • Renewal: the site gave the anonymous visitor a session before login (Ae5f…); that value differs from the new one and, when replayed, lands on the login page.
  • Logout: the delete repeats Path=/, and replaying the old value with curl (-i prints the response headers, -H adds a request header), for example curl -i -H "Cookie: sid=k3Vq9XbT2mR7wLp4" https://portal.example/dashboard, returns a redirect to /login, never dashboard data.
  • Idle timeout at its boundary: network delay and the server's timer make the exact second unreliable, so agree on a small margin and test both sides, for example a request after 14 min 50 s idle (still works) and one after 15 min 10 s (login page). Ask whether the test environment can use shorter timeouts.
  • Absolute timeout: despite steady activity, the session ends 8 hours after login.
  • Scope: docs.portal.example never receives sid.
POST /login HTTP/1.1
Host: portal.example
Cookie: sid=Ae5fUq0Zr8Ty2Nc6

HTTP/1.1 303 See Other
Location: /dashboard
Set-Cookie: sid=k3Vq9XbT2mR7wLp4; Path=/; Secure; HttpOnly; SameSite=Lax

GET /dashboard HTTP/1.1
Host: portal.example
Cookie: sid=k3Vq9XbT2mR7wLp4

POST /logout HTTP/1.1
Host: portal.example
Cookie: sid=k3Vq9XbT2mR7wLp4

HTTP/1.1 303 See Other
Location: /login
Set-Cookie: sid=; Path=/; Max-Age=0; Secure; HttpOnly; SameSite=Lax

Try it: Cookie Analyzer Try it: cURL Generator

Common mistakes

  • Passing logout because the cookie vanished from the browser, without replaying the old identifier against the server.
  • Looking for Secure or HttpOnly in the request's Cookie header; attributes appear only in Set-Cookie.
  • Treating the cookie's Max-Age as the session timeout instead of testing the timeout the server enforces.
  • Assuming that HttpOnly stops cross-site scripting (XSS) or that SameSite stops every forged request.
  • Ignoring Domain and Path: a session cookie scoped to the parent domain, or a logout that deletes with a different Path.
  • Pasting live session identifiers into bug reports, chats or screenshots instead of masking them.

The QA perspective

Session behaviour usually comes from framework configuration, often left at its defaults, so ask early for the session policy: cookie name and attributes, both timeouts, concurrent sessions, and what logout and a password change do. As a short table, it becomes test basis that developers and security reviewers can confirm; any blank is a requirement question.

Keep these checks in the regression suite and rerun them after framework upgrades, proxy or CDN changes and domain moves, which change cookies silently. Report the masked request and response, the browser version and timestamps, and leave deeper attacks on session handling to security specialists and the OWASP Web Security Testing Guide.

Practice exercise

Review the session cookie of a clinic portal

Your task

A clinic's booking portal in a test environment sends the headers below. A colleague notes that after logout the browser still sends the old SESSION value, and a curl replay of it returns 200 OK with the user's appointments. List every defect or risk, the rule it breaks, and a confirmation test for each fix.

Material
Response to POST /login on https://clinic.example
Set-Cookie: SESSION=u1042-20261002-0915; Domain=clinic.example; Path=/; Max-Age=2592000

Response to POST /logout
Set-Cookie: SESSION=; Path=/app; Max-Age=0

Login of a second test user at 11:30
Set-Cookie: SESSION=u1043-20261002-1130; Domain=clinic.example; Path=/; Max-Age=2592000
Steps
  1. Read the login Set-Cookie attribute by attribute, or paste it into the Cookie Analyzer.
  2. Compare the logout Set-Cookie with it: name, Domain and Path.
  3. Decide what the replay says about the server-side session.
Show the model answer
Model answer

Predictable identifier: user number plus login date and time, as the second value confirms, so other users' identifiers can be guessed. It must be random, with at least 64 bits of entropy. Test: values from repeated logins contain no user number or time and show no pattern, and code review confirms a cryptographically secure generator.

Secure, HttpOnly and SameSite are missing, Domain=clinic.example reaches every subdomain, and Max-Age=2592000 keeps the cookie 30 days through restarts, against OWASP's advice on non-persistent session cookies; ask what the requirement says about "remember me". Test: the Cookie Analyzer shows the three attributes and no Domain or Max-Age.

The delete uses Path=/app and no Domain, so it misses the original cookie, which the browser keeps: a cookie is identified by name, domain and path together. It must repeat the name, Domain and Path the login cookie actually uses (after the previous fix: no Domain, Path=/). Test: after logout the browser's cookie list has no SESSION, and the next request carries no SESSION cookie.

Most serious: the server did not end the session, although logout must end the record on the server, so anyone holding the old value sees the user's appointments. Severity: critical. Test: a replay after logout gets a login redirect or 401; add tests for renewal and both timeouts.

Frequently asked questions

What is the difference between a session cookie and a session?

A session cookie is any cookie without Expires or Max-Age; the browser drops it when its own session ends, although a browser that restores the previous session on restart may keep it. A session is the record on the server that an identifier points to. The identifier may sit in a session cookie or a persistent one, and removing the cookie does not end the server's session, which is why logout and timeouts are tested on the server side.

How do I test a 30-minute idle timeout without waiting 30 minutes every time?

Ask the developers whether the timeout is configurable, and use a short value such as two minutes in the test environment to test the mechanism with boundary values. Then confirm the real value once, in the configuration or with one long test, in an environment configured like production. Editing the cookie's expiry in the browser proves nothing, because the server's clock decides.

Should an expired session get a 401 or a redirect?

It depends on the client. A browser page usually gets a redirect such as 302 or 303 to the login page, and an API returns 401 Unauthorized. What must never happen is a 200 with the user's data, or an error page with a stack trace. Check that the agreed behaviour is the same on every protected route.

Does SameSite protect against cross-site request forgery on its own?

Cross-site request forgery (CSRF) means that another site makes a logged-in user's browser send an unwanted request to your application, with the user's cookies attached. SameSite reduces that risk, but the OWASP Cross-Site Request Forgery Prevention Cheat Sheet treats it as defence in depth, not as a replacement for a proper CSRF defence such as anti-CSRF tokens. Lax still sends the cookie on top-level GET navigations from other sites, so any state-changing action reachable with GET stays exposed. Test both the attribute and the token.

References

Practise with Mutqan tools

Parse Set-Cookie headers and check Secure, HttpOnly, SameSite, Domain, Path, expiry and size best practices.

Validation

Parse raw HTTP request/response headers into a structured table with explanations.

Web

Build cURL commands from method, URL, headers, body and auth options.

Web

Break a URL into protocol, host, port, path, query parameters, fragment and credentials.

Web

Go deeper

Your progress on this topic

Part of the QA Roadmap