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

Sponsored

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.cookie and exfiltrate the session ID unless the cookie is marked HttpOnly.
  • Eavesdropping — Over a plain HTTP connection, anyone on the network can read the cookie. The Secure flag 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 SameSite flag 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 HttpOnly flag 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 windowHttpOnly (JavaScript can't peek at document.cookie)
  • Only usable over a secure corridorSecure (only sent over HTTPS)
  • Not usable by neighboring roomsSameSite (don't attach to cross-site requests)
  • Expiring dailyExpires / Max-Age (limits window of abuse)
  • Signed by the hotel's master keySignature / 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:

  1. User logs in — Your app validates credentials, then generates a session ID (a random string) and stores it in a signed cookie (session in Flask).
  2. Server responds — The Set-Cookie header includes the session ID and any flags you set: Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Lax.
  3. Browser stores the cookie — It respects the flags: keeps it out of JavaScript, only sends over HTTPS, and appends it to requests per SameSite rules.
  4. 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_SAMESITE in settings.py.
  • FastAPI — you're likely using httponly cookies via Response.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://localhost and the session cookie never appears.
  • Cause: Secure=True tells 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 to False, but never deploy without it.

2. SameSite=None without Secure

  • Error: Browser rejects the cookie (Chrome requires Secure with SameSite=None).
  • Fix: Set both SameSite=None and Secure=True when you truly need cross-site requests (OAuth flows, third-party embeds).

3. User gets logged out too often with SameSite=Strict

  • Cause: Strict prevents sending the cookie on any cross-site navigation, including clicking a link from an email.
  • Fix: Use Lax and add CSRF tokens for critical POST endpoints.

4. Cookie not expiring

  • Cause: session.permanent not set, or PERMANENT_SESSION_LIFETIME not assigned.
  • Fix: Set session.permanent = True on login, and define PERMANENT_SESSION_LIFETIME.

5. Missing HttpOnly on custom cookies

  • Error: Your custom cookie is readable by JavaScript because set_cookie defaults to httponly=False.
  • Fix: Always pass httponly=True explicitly when using set_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=True makes the cookie work only over HTTPS — if you test over HTTP, the cookie silently disappears (sessions 'fail').
  • Setting SameSite=None without Secure=True — modern browsers reject this combination, blocking the cookie entirely.
  • Assuming Flask's default session cookie has HttpOnly=False — it's actually True, but custom cookies set via set_cookie() default to httponly=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

  1. 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).
  2. In Django, configure secure cookies via SESSION_COOKIE_SECURE, SESSION_COOKIE_HTTPONLY, and SESSION_COOKIE_SAMESITE in settings.py.
  3. Implement CSRF tokens (e.g., Flask-WTF) as a compensating control when you must relax SameSite to None for 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=True in production, even if it's annoying in local dev; use HTTPS locally instead.
  • HttpOnly=True keeps JavaScript from reading the cookie — a cheap, effective defense against XSS-based theft.
  • Choose SameSite=Lax as a balanced default; reserve Strict for high-security scenarios and None only when necessary.
  • Configure session expiration (Max-Age / PERMANENT_SESSION_LIFETIME) to limit the window of abuse.
  • Inspect your Set-Cookie headers with curl or browser dev tools to verify your flags are actually applied.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.