Skip to main contentLogo

Command Palette

Search for a command to run...

Cookies — The Indispensable Memory of Web Applications

Published on
Apr 14, 2025
Cookies — The Indispensable Memory of Web Applications

Cookies: The Indispensable Memory of Web Applications

Hello! Previously we talked about HTTP’s stateless nature. So how do websites “recognize” us, keep us logged in, and preserve our shopping carts despite this memoryless protocol? The answer often lies in small text files: cookies. Today we’ll dive deep—from how they’re created to security risks and modern alternatives. Ready? Let’s open the cookie jar!

A cookie’s life begins with an HTTP response header from the server: Set-Cookie. When the browser sees this header, it stores the cookie according to the directives and sends it back on future matching requests via the Cookie header.

A typical Set-Cookie header might look like this:

responses/set-cookie.txt
HTTP/1.1 200 OK
Content-Type: text/html
Set-Cookie: sessionId=a3fWa; Expires=Wed, 21 Oct 2026 07:28:00 GMT; Path=/; Domain=.example.com; Secure; HttpOnly; SameSite=Lax // [!code highlight]

Let’s analyze the directives in detail:

  • Name=Value: The main payload. sessionId=a3fWa shows the cookie’s name (sessionId) and value (a3fWa). Used by the server to identify the user or their state.
  • Expires=Date: Expiration date. After this, the browser deletes the cookie. If neither Expires nor Max-Age is set, it’s a session cookie and is deleted when the browser session closes.
  • Max-Age=Seconds: How many seconds the cookie lives. More modern than Expires and preferred if both are set. Negative deletes immediately. Example: Max-Age=3600 → 1 hour.
  • Domain=domain.name: Which domain(s) the cookie will be sent to. If omitted, only the originating host. A value starting with . (e.g., .example.com) covers the main domain and all subdomains.
  • Path=path: Limits which URL paths receive the cookie. / for all paths; /app for /app and its subpaths.
  • 🛡️ Secure: Only send the cookie over HTTPS. Prevents sending over HTTP, reducing MITM risk. Must be used!
  • 🛡️ HttpOnly: Prevents access via JavaScript (document.cookie). Critical to protect session cookies from XSS theft.
  • 🛡️ SameSite=Strict | Lax | None: Helps mitigate CSRF by controlling whether to send cookies with cross-site requests:
    • Strict: Only same-site requests. Not sent even when navigating via external links. Safest, may impact UX.
    • Lax: Default in many modern browsers. Sent with same-site requests and top-level cross-site navigations (GET via link). Not sent with cross-site POST/PUT/DELETE or in <iframe>, <img>. Good balance.
    • None: Sent with all cross-site requests, but requires Secure. Used mainly in third-party contexts.

Practical Use Cases: Session Management and UX

Cookies primarily compensate for HTTP’s statelessness:

  • Session Management:
    • On login, the server creates a unique session identifier.
    • Sends it in Set-Cookie (usually with HttpOnly and Secure).
    • The browser stores it and sends it back in subsequent requests (Cookie: sessionId=...).
    • The server uses the session ID to fetch session data (who is logged in, permissions) from server-side storage and “recognizes” the user.
    • Result: The user doesn’t need to log in on every page—foundation of a stateful experience.
  • User Experience and Personalization:
    • Preferences (language, theme), recently viewed items.
    • Shopping cart for even non-logged-in users (though more complex solutions often use backend sessions or localStorage).
    • Tracking & Analytics (e.g., Google Analytics), often via third-party cookies—hence privacy concerns.

If a cookie isn’t HttpOnly, it can be accessed via client-side JavaScript:

JavaScript
client/document-cookie.js
// Read all accessible cookies (string)
let allCookies = document.cookie;
console.log(allCookies); // e.g., "pref=dark; lang=en; other=value"

// Write a new cookie or update an existing one
// Note: This doesn’t delete other cookies—only adds/updates the specified one
document.cookie = "username=johndoe; expires=Fri, 25 Apr 2026 20:36:32 GMT; path=/; SameSite=Lax"; 

// Delete a cookie by setting Expires to a past date
document.cookie = "username=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/"; 

// Using Max-Age
document.cookie = "theme=dark; max-age=86400; path=/"; // 86400 seconds = 1 day

⚠️ Note: The document.cookie API is clunky. It returns a single string you must parse, and updating requires re-specifying all attributes. Most importantly, if your site has an XSS vulnerability, all non-HttpOnly cookies (including those set via JS) can be stolen! Use HttpOnly to keep sensitive data (session IDs, tokens) inaccessible to JS.

Backend Integration: Working with Cookies in Spring Boot

On the backend (e.g., Java Spring Boot), working with cookies is more controlled and secure.

  • Reading Cookies:

    • @CookieValue annotation: Easiest approach. Spring injects the cookie value into a method parameter.
    controller/CookieController.java
    import org.springframework.web.bind.annotation.CookieValue;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class CookieController {
    
        @GetMapping("/read-session")
        public String readSessionCookie(@CookieValue(name = "sessionId", defaultValue = "guest") String sessionId) { 
            // use sessionId...
            return "Session ID from cookie: " + sessionId;
        }
    }
    • HttpServletRequest: To get all cookies as an array.
    controller/CookieControllerReadAll.java
    import jakarta.servlet.http.Cookie;
    import jakarta.servlet.http.HttpServletRequest;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    import java.util.Arrays;
    import java.util.Optional;
    
    @RestController
    public class CookieController {
    
        @GetMapping("/read-all")
        public String readAllCookies(HttpServletRequest request) {
            Cookie[] cookies = request.getCookies();
            if (cookies != null) {
                Optional<Cookie> themeCookie = Arrays.stream(cookies)
                            .filter(cookie -> "theme".equals(cookie.getName()))
                            .findFirst();
    
                if (themeCookie.isPresent()) {
                    return "Theme: " + themeCookie.get().getValue();
                }
            }
            return "No theme cookie found.";
        }
    }
  • Writing/Setting Cookies:

    • HttpServletResponse.addCookie() (Traditional): Create a jakarta.servlet.http.Cookie and add it to the response.
    controller/CookieControllerSetTheme.java
    import jakarta.servlet.http.Cookie;
    import jakarta.servlet.http.HttpServletResponse;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class CookieController {
    
        @GetMapping("/set-theme")
        public String setThemeCookie(HttpServletResponse response) {
            Cookie themeCookie = new Cookie("theme", "dark");
            themeCookie.setPath("/");
            themeCookie.setMaxAge(86400); // 1 day
            themeCookie.setHttpOnly(true); 
            themeCookie.setSecure(true); // requires HTTPS
            // themeCookie.setDomain(".example.com"); // if needed
            response.addCookie(themeCookie); 
            return "Theme cookie set to dark.";
        }
    }
    • ResponseCookie (Spring 5+ — Modern): Fluent builder to set modern attributes (especially SameSite) easily. Used with HttpHeaders.SET_COOKIE.
    controller/CookieControllerModern.java
    import org.springframework.http.HttpHeaders;
    import org.springframework.http.ResponseCookie;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    import java.time.Duration;
    
    @RestController
    public class CookieController {
    
        @GetMapping("/set-session-modern")
        public ResponseEntity<String> setSessionCookieModern() {
            String sessionId = generateSecureSessionId();
    
            ResponseCookie sessionCookie = ResponseCookie.from("sessionId", sessionId)
                        .httpOnly(true) 
                        .secure(true) // must be true in production
                        .path("/")
                        .maxAge(Duration.ofHours(1)) // 1 hour
                        .sameSite("Lax") // CSRF protection
                        // .domain(".example.com") // set domain for production
                        .build();
    
            return ResponseEntity.ok()
                        .header(HttpHeaders.SET_COOKIE, sessionCookie.toString()) 
                        .body("Modern session cookie set.");
        }
    
        private String generateSecureSessionId() {
            // Use a cryptographically secure session ID generator
            return java.util.UUID.randomUUID().toString();
        }
    }

    ⚙️ Using ResponseCookie helps write clearer code and ensures all essential security attributes are considered.

On the Security Radar: Risks and Defenses

Cookies can be as dangerous as they are useful. Key risks and mitigations:

  • XSS (Cross-Site Scripting):
    • Risk: If your site has an XSS vulnerability, an attacker can inject JS in the user’s browser and exfiltrate any non-HttpOnly cookies (e.g., session ID) via document.cookie, leading to session hijacking.
    • 🛡️ Defense: Enable HttpOnlymandatory. Also implement input validation and output encoding.
  • CSRF (Cross-Site Request Forgery):
    • Risk: Browsers automatically attach cookies on requests to the same domain. Attackers can trick users into sending requests from another site (e.g., evil.com) to a site they’re logged into (e.g., bank.com).
    • 🛡️ Defense: Set SameSite to Lax (default) or Strict to limit cross-site cookie sending. Additionally, use anti-CSRF tokens for state-changing requests (POST/PUT/DELETE).
  • Session Hijacking / Fixation:
    • Risk: Cookies can be sniffed (without HTTPS), stolen via XSS, or a fixed session ID can be forced on a user.
    • 🛡️ Defense: Always use HTTPS (with Secure). Use HttpOnly. Regenerate a new session ID immediately after login and invalidate the old one.

Cookies by context:

  • First-party cookie: Set by the domain the user is visiting (e.g., example.com while on example.com).
  • Third-party cookie: Set by a different domain embedded in the page (ads, social widgets, analytics).

⚠️ Trend: Due to privacy concerns (especially cross-site tracking), most modern browsers (Safari, Firefox, and gradually Chrome) are blocking or restricting third-party cookies. This impacts ad/analytics ecosystems and accelerates alternatives (e.g., Google Privacy Sandbox).

Alternatives to Cookies: Modern Approaches

  • JWT (JSON Web Tokens):
    • Popular for stateless authentication. On login, the server issues a signed token to the client.
    • The client stores it (often in localStorage or sessionStorage) and sends it in Authorization: Bearer <token> for protected requests.
    • No server-side session store required.
    • Comparison: More resilient to CSRF than cookies (not sent automatically), but vulnerable to XSS if stored in localStorage.
  • Web Storage API (localStorage/sessionStorage):
    • Key-value storage in the browser (more space than cookies, typically 5–10MB).
    • localStorage persists across browser restarts; sessionStorage persists for the life of the tab/window.
    • Comparison: Unlike cookies, not sent automatically to the server; must be read and added to requests via JS. Also XSS-exposed. Not recommended for sensitive session data; better for UI state, preferences, and offline data.
Thanks for reading.