HttpOnlyのないCookieの露出

HttpOnlyのない機密Cookieへのスクリプトからのアクセス

説明

認証情報やセッション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を送信できます。

参考資料