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.cookieand 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: trueon session and authentication cookies. - Apply both
HttpOnlyandSecureto 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_idcookie lacks HttpOnly, allowing browser JavaScript to read it throughdocument.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,SecureandSameSite, andListenAndServeTLSprovides HTTPS so the Secure cookie can be sent.