Cookie exposure without HttpOnly

Script access to sensitive cookies without HttpOnly

Description

Without HttpOnly, client-side scripts can read sensitive cookie values, such as authentication information or session IDs, through document.cookie. An attacker exploiting XSS may inject a script that steals the session cookie and uses it to hijack the user's session.

Potential impact

  • XSS may let an attacker read a session cookie through document.cookie and take over the session.
  • The stolen session may allow actions within the user's permissions, including viewing sensitive data, changing settings or making payments.
  • Theft of an administrator's or another powerful account's cookie may enable configuration changes, extensive data exposure or fraudulent transactions.

Remediation

  • Set HttpOnly: true on session and authentication cookies.
  • Apply both HttpOnly and Secure to sensitive cookies, using HTTPS for transmission.
  • Set the required protection attributes by default in shared cookie creation, deletion and renewal helpers.
  • Fix XSS vulnerabilities too. HttpOnly prevents reading the cookie value, but does not stop a malicious script from sending requests authenticated as the user.

Examples

These examples compare cookie attributes. User authentication and server-side session storage are not implemented; both are required for an actual login. Supply deployment-appropriate certificates and keys for the HTTPS example.

Before

go
package main

import (
    "crypto/rand"
    "encoding/base64"
    "log"
    "net/http"
    "time"
)

func newSessionID() (string, error) {
    token := make([]byte, 32)
    if _, err := rand.Read(token); err != nil {
        return "", err
    }
    return base64.RawURLEncoding.EncodeToString(token), nil
}

// Before: session cookie without HttpOnly
func setSessionCookie(w http.ResponseWriter, sessionID string) {
    cookie := http.Cookie{
        Name:    "session_id",
        Value:   sessionID,
        Path:    "/",
        // HttpOnly and Secure are missing
        Expires: time.Now().Add(30 * time.Minute),
    }
    http.SetCookie(w, &cookie)
}

func handler(w http.ResponseWriter, r *http.Request) {
    sessionID, err := newSessionID()
    if err != nil {
        http.Error(w, "session creation failed", http.StatusInternalServerError)
        return
    }
    setSessionCookie(w, sessionID)
    w.Write([]byte("ok"))
}

func main() {
    http.HandleFunc("/login", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

After

go
package main

import (
    "crypto/rand"
    "encoding/base64"
    "log"
    "net/http"
    "time"
)

func newSessionID() (string, error) {
    token := make([]byte, 32)
    if _, err := rand.Read(token); err != nil {
        return "", err
    }
    return base64.RawURLEncoding.EncodeToString(token), nil
}

// After: set HttpOnly and Secure in a shared helper
func setSecureSessionCookie(w http.ResponseWriter, sessionID string) {
    cookie := http.Cookie{
        Name:     "session_id",
        Value:    sessionID,
        Path:     "/",
        Expires:  time.Now().Add(30 * time.Minute),
        HttpOnly: true,          // Not readable by JS
        Secure:   true,          // Send only over HTTPS
        SameSite: http.SameSiteLaxMode,
    }
    http.SetCookie(w, &cookie)
}

func handler(w http.ResponseWriter, r *http.Request) {
    sessionID, err := newSessionID()
    if err != nil {
        http.Error(w, "session creation failed", http.StatusInternalServerError)
        return
    }
    // Store the association between the authenticated user and sessionID in server-side session storage.
    setSecureSessionCookie(w, sessionID)
    w.Write([]byte("ok"))
}

func main() {
    http.HandleFunc("/login", handler)
    log.Fatal(http.ListenAndServeTLS(":8443", "server.crt", "server.key", nil))
}

Explanation:

  • Before: The session_id cookie lacks HttpOnly, allowing browser JavaScript to read it through document.cookie. XSS may therefore expose the session and enable account takeover. The missing Secure flag also permits exposure through HTTP traffic.
  • After: The server generates a session ID using cryptographically secure randomness. Linking it to the authenticated user and storing that association, as indicated in the comment, still need implementation. The shared helper sets HttpOnly, Secure and SameSite, and ListenAndServeTLS provides HTTPS so the Secure cookie can be sent.

References