説明
認証情報やセッションIDなどの機密CookieにHttpOnlyがないと、クライアント側のスクリプトが document.cookie で値を読めます。攻撃者がXSSを悪用してスクリプトを挿入し、セッションCookieを盗んでユーザーのセッションを乗っ取るおそれがあります。
想定される影響
- XSSで
document.cookieからセッションCookieを読まれ、セッションを乗っ取られる可能性があります。 - 盗まれたセッションを使い、ユーザーの権限で機密情報の閲覧、設定変更、決済などを行われるおそれがあります。
- 管理者など強い権限を持つアカウントのCookieが盗まれると、システム設定の変更、大量のデータ漏えい、不正な取引につながる可能性があります。
対処方法
- セッション・認証Cookieに
HttpOnly: trueを設定してください。 - 機密Cookieには
HttpOnlyと、HTTPSでの送信を要求するSecureの両方を適用してください。 - Cookieの作成・削除・更新用の共通関数で、必要な保護属性を既定値にしてください。
- XSS自体も修正してください。HttpOnlyはCookieの値の読み取りを防ぎますが、悪意のあるスクリプトがユーザーとして認証されたリクエストを送ることまでは防ぎません。
例
Cookie属性を比較する例です。ユーザー認証とサーバー側のセッション保存は未実装で、実際のログイン処理には両方が必要です。HTTPS例の証明書と鍵も、デプロイ環境に合わせて用意してください。
変更前
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
}
// 変更前: セッションCookieにHttpOnlyがない
func setSessionCookie(w http.ResponseWriter, sessionID string) {
cookie := http.Cookie{
Name: "session_id",
Value: sessionID,
Path: "/",
// HttpOnlyとSecureがない
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))
}
変更後
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
}
// 変更後: 共通関数でHttpOnlyとSecureを設定
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, // JSから読み取り不可
Secure: true, // 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
}
// 認証済みユーザーとsessionIDの関連付けはサーバー側のセッションストアに保存する。
setSecureSessionCookie(w, sessionID)
w.Write([]byte("ok"))
}
func main() {
http.HandleFunc("/login", handler)
log.Fatal(http.ListenAndServeTLS(":8443", "server.crt", "server.key", nil))
}
解説:
- 変更前:
session_idにHttpOnlyがなく、ブラウザーのJavaScriptがdocument.cookieで読めます。XSSによってセッションを盗まれ、アカウントの乗っ取りにつながるおそれがあります。Secureもなく、HTTP通信での露出も起こり得ます。 - 変更後: 暗号学的に安全な乱数でセッションIDを生成します。コメントに示した認証済みユーザーとの関連付けと保存は、別途実装が必要です。共通関数が
HttpOnly、Secure、SameSiteを設定し、ListenAndServeTLSでHTTPSを提供するため、Secure Cookieを送信できます。