Secure Cookies & Session Flags
Apply secure cookies and session flags to protect your web app. Learn to set HttpOnly, Secure, SameSite, and other critical session attributes to prevent hijacking and cross-site attacks.
Focus: apply secure cookies and session flags
You’ve just finished building your Flask app’s login endpoint, and it feels great — users can sign in, and you’re storing their session ID in a cookie. But that seemingly innocent cookie is a ticking time bomb: if you haven’t locked it down with the right flags, an attacker can steal it via cross-site scripting, sniff it over an unencrypted connection, or trick the browser into sending it with malicious cross-site requests. Session hijacking, CSRF, and credential theft are not hypotheticals — they are the top causes of real-world web breaches. In this lesson, you’ll learn how to apply secure cookies and session flags — the exact attributes (Secure, HttpOnly, SameSite, and more) that turn a fragile session cookie into a hardened credential.
The problem this lesson solves
A web app’s session cookie is its crown jewels: it tells the server who you are after login. But without proper flags, your cookie is an open door for attackers:
- Theft — If an attacker can execute JavaScript in your page (e.g., via a stored XSS), they can read
document.cookieand exfiltrate the session ID unless the cookie is markedHttpOnly. - Eavesdropping — Over a plain HTTP connection, anyone on the network can read the cookie. The
Secureflag makes the browser refuse to send it over HTTP, but if you don't set it, the cookie travels in cleartext. - CSRF — A malicious site can make your browser send a request to your app with your session cookie, because cookies are automatically attached to requests. The
SameSiteflag controls when that happens, blocking many cross-site request forgery attacks. - Cookie tampering — If you don't add integrity checks or expiration, an attacker might modify the cookie value to impersonate another user.
Tip: The cost of ignoring these flags is measured in breached accounts. A single
HttpOnlyflag can neutralize an entire class of XSS-based session theft — it costs one line of code.
Core concept / mental model
Think of a session cookie as a hotel keycard. The keycard alone doesn't grant access — the server keeps a list of who holds which card. But the card is only safe if it’s:
- Not visible through the window →
HttpOnly(JavaScript can't peek atdocument.cookie) - Only usable over a secure corridor →
Secure(only sent over HTTPS) - Not usable by neighboring rooms →
SameSite(don't attach to cross-site requests) - Expiring daily →
Expires/Max-Age(limits window of abuse) - Signed by the hotel's master key →
Signature/ integrity (prevents tampering)
In HTTPS session management, every flag is a defense layer. Remove one, and the whole system weakens. The table below shows the core flags you’ll apply in Python web frameworks:
| Flag / attribute | Purpose | Default in Flask? | Recommended value |
|---|---|---|---|
Secure |
Sent only over HTTPS | No | True |
HttpOnly |
Inaccessible to JavaScript | Yes (Flask sets it) | True |
SameSite |
Restricts cross-site sending | Lax in Flask if set |
Lax or Strict |
Max-Age / Expires |
Cookie lifetime | Session cookie (browser close) | e.g., 3600 seconds |
Path |
Limits URL scope | / |
App's root |
Domain |
Limits host scope | Host only | Omit unless subdomains needed |
The mental model: each flag is a security policy enforced by the browser — not by your server. Your job is to configure them correctly so the browser behaves as your gatekeeper.
How it works step by step
Let's see how these flags come to life in a typical Flask session flow:
- User logs in — Your app validates credentials, then generates a session ID (a random string) and stores it in a signed cookie (
sessionin Flask). - Server responds — The
Set-Cookieheader includes the session ID and any flags you set:Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax. - Browser stores the cookie — It respects the flags: keeps it out of JavaScript, only sends over HTTPS, and appends it to requests per
SameSiterules. - Next request — Browser attaches the cookie (if allowed by
SameSite), and your server decodes it to identify the user.
In Flask, you configure session cookies via SESSION_COOKIE_* config values. Let's look at the critical steps in code.
Step 1: Enable HTTPS and set Secure
Run your app behind HTTPS (deployment concern), then set SESSION_COOKIE_SECURE = True. This prevents cookie transmission over HTTP — even if a user types http:// manually.
Step 2: Set HttpOnly
Flask sets HttpOnly by default, but if you're using raw Response objects, you must add it manually. The flag tells the browser to hide the cookie from JavaScript — preventing XSS-based theft.
Step 3: Configure SameSite
Set SESSION_COOKIE_SAMESITE = 'Lax' or 'Strict'. Lax sends the cookie on top-level navigation (e.g., clicking a link from an external site) but not on cross-site subresource requests like images or AJAX — enough to block CSRF while preserving usability.
Step 4: Set expiration & domain
Use PERMANENT_SESSION_LIFETIME (a timedelta) and session.permanent = True to get a persistent cookie with Max-Age. Set SESSION_COOKIE_DOMAIN only if you need cookie sharing across subdomains — otherwise leave it empty to avoid over-scoping.
Hands-on walkthrough
Let's apply all this in a minimal Flask app. First, install Flask if you haven't:
pip install flask
Example 1: Baseline Flask session with secure flags
from flask import Flask, session, redirect, url_for
from datetime import timedelta
app = Flask(__name__)
app.secret_key = 'a-very-strong-random-secret'
# --- Secure cookie configuration ---
app.config.update(
SESSION_COOKIE_SECURE=True, # HTTPS only
SESSION_COOKIE_HTTPONLY=True, # JS cannot read
SESSION_COOKIE_SAMESITE='Lax', # CSRF protection
SESSION_COOKIE_NAME='myapp_session', # rename from 'session'
PERMANENT_SESSION_LIFETIME=timedelta(hours=2),
)
@app.route('/')
def index():
session.permanent = True # use PERMANENT_SESSION_LIFETIME
if 'username' in session:
return f"Logged in as {session['username']}"
return "Not logged in"
@app.route('/login/<username>')
def login(username):
session['username'] = username
return redirect(url_for('index'))
if __name__ == '__main__':
app.run(ssl_context='adhoc') # for local HTTPS testing
Expected behavior: When you visit https://localhost:5000/login/alice, the response includes:
Set-Cookie: myapp_session=...; HttpOnly; Secure; SameSite=Lax
Example 2: Inspect the Set-Cookie header (using curl)
curl -I -k https://localhost:5000/login/alice
Output snippet:
HTTP/1.1 302 FOUND
Set-Cookie: myapp_session=...; HttpOnly; Secure; SameSite=Lax; Path=/
Example 3: For raw responses (e.g., custom APIs)
If you're not using Flask's session, you can set cookie flags directly on a Response:
from flask import Response, make_response
@app.route('/set-cookie')
def set_cookie():
resp = make_response("Cookie set!")
resp.set_cookie(
'auth_token',
'secret-value',
secure=True,
httponly=True,
samesite='Strict',
max_age=3600,
path='/api'
)
return resp
Now the Set-Cookie header will include all flags.
Pro tip: Always set
SESSION_COOKIE_HTTPONLY=True(even though Flask does it) to make your intent explicit — future devs will thank you.
Compare options / when to choose what
Different flags and frameworks offer trade-offs. Let's compare common configurations:
| Configuration | Security Level | Usability | Use case |
|---|---|---|---|
SameSite=None + Secure |
Low (needs CSRF token) | High | Cross-site embeds / third-party widgets |
SameSite=Lax (default) |
Medium | High | Most web apps |
SameSite=Strict |
High | Low | High-security apps where UX can tolerate stricter rules (e.g., banking) |
Secure=False |
Very Low | High | Dev over HTTP only (never for production) |
HttpOnly=False |
Very Low | Low | Only if JavaScript must access cookie (avoid if possible) |
Choosing in Python frameworks:
- Flask/Django/Rails — use built-in config settings as shown above.
- Django — set
SESSION_COOKIE_SECURE,SESSION_COOKIE_HTTPONLY,SESSION_COOKIE_SAMESITEinsettings.py. - FastAPI — you're likely using
httponlycookies viaResponse.set_cookie.
Rule of thumb: Start with
Secure=True,HttpOnly=True,SameSite=Lax. Only loosen when you have a concrete need and a compensating control (like CSRF tokens).
Troubleshooting & edge cases
1. Secure flag and HTTP development
- Error: You test over
http://localhostand the session cookie never appears. - Cause:
Secure=Truetells the browser to drop cookies over non-HTTPS connections. - Fix: Use HTTPS locally (e.g.,
ssl_context='adhoc'in Flask, or a reverse proxy). For dev, you may temporarily set it toFalse, but never deploy without it.
2. SameSite=None without Secure
- Error: Browser rejects the cookie (Chrome requires
SecurewithSameSite=None). - Fix: Set both
SameSite=NoneandSecure=Truewhen you truly need cross-site requests (OAuth flows, third-party embeds).
3. User gets logged out too often with SameSite=Strict
- Cause:
Strictprevents sending the cookie on any cross-site navigation, including clicking a link from an email. - Fix: Use
Laxand add CSRF tokens for critical POST endpoints.
4. Cookie not expiring
- Cause:
session.permanentnot set, orPERMANENT_SESSION_LIFETIMEnot assigned. - Fix: Set
session.permanent = Trueon login, and definePERMANENT_SESSION_LIFETIME.
5. Missing HttpOnly on custom cookies
- Error: Your custom cookie is readable by JavaScript because
set_cookiedefaults tohttponly=False. - Fix: Always pass
httponly=Trueexplicitly when usingset_cookie.
What you learned & what's next
You can now explain the core idea behind secure cookies and session flags: each flag is a browser-enforced security policy that protects the session token from theft, eavesdropping, and cross-site misuse. You also completed a practical exercise by configuring a Flask app with Secure, HttpOnly, SameSite, Max-Age, and Path flags — and you inspected the resulting Set-Cookie header. You connected the lesson to broader secure development by seeing how these flags mitigate XSS, CSRF, and MITM attacks.
Now that your session cookies are locked down, it's time to tackle the next frontier: input validation and output encoding — because even a secure cookie can't save you if your app is vulnerable to SQL injection or command injection. Head over to the next lesson in the Secure development track.
Practice recap
Try extending the example app: add a logout endpoint that clears the session and verify flags appear in the response headers using curl -I. Then experiment with SameSite=Strict and Lax by sending a cross-site request — observe when the cookie is sent.
Common mistakes
- Forgetting that
Secure=Truemakes the cookie work only over HTTPS — if you test over HTTP, the cookie silently disappears (sessions 'fail'). - Setting
SameSite=NonewithoutSecure=True— modern browsers reject this combination, blocking the cookie entirely. - Assuming Flask's default session cookie has
HttpOnly=False— it's actuallyTrue, but custom cookies set viaset_cookie()default tohttponly=False. - Not setting a max-age or permanent session lifetime, so cookies become non-persistent and users are logged out on every browser close.
Variations
- Use
SESSION_COOKIE_SAMESITE='Strict'if your app can tolerate stricter CSRF protection at the cost of some cross-site UX (e.g., internal tools). - In Django, configure secure cookies via
SESSION_COOKIE_SECURE,SESSION_COOKIE_HTTPONLY, andSESSION_COOKIE_SAMESITEinsettings.py. - Implement CSRF tokens (e.g., Flask-WTF) as a compensating control when you must relax
SameSitetoNonefor cross-site flows.
Real-world use cases
- E-commerce site: secure cookies with HttpOnly and SameSite=Lax prevent XSS-based session theft, keeping users logged in while browsing products.
- Banking portal: Strict SameSite + Secure + HttpOnly block CSRF and session hijacking even if a user clicks a malicious link.
- OAuth provider: uses SameSite=None + Secure so third-party apps can set sessions cross-site while still protecting the token with HttpOnly.
Key takeaways
- Secure cookies defend against three major attacks: session hijacking (HttpOnly), MITM eavesdropping (Secure), and CSRF (SameSite).
- Always set
Secure=Truein production, even if it's annoying in local dev; use HTTPS locally instead. HttpOnly=Truekeeps JavaScript from reading the cookie — a cheap, effective defense against XSS-based theft.- Choose
SameSite=Laxas a balanced default; reserveStrictfor high-security scenarios andNoneonly when necessary. - Configure session expiration (
Max-Age/PERMANENT_SESSION_LIFETIME) to limit the window of abuse. - Inspect your
Set-Cookieheaders with curl or browser dev tools to verify your flags are actually applied.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.