Build Web Application With Golang を試してやってみる 23

はじめに

前回の記事の続きです。
前回の記事はこちら

sessionとデータの保存

今回はsession について理解していきます。
今週はサボりすぎたので、この章はまとめていく予定です。

sessionとcookie

sessioncookie についての説明です。
簡単に言うと、クライアント側でのデータ保存か(cookie)、サーバ側でのデータ保存(session)です。

cookiesession の説明があるので、引用すると下記のようなものです。

cookieとは、簡単に言えばローカルマシンに保存されたユーザの操作の履歴情報です(当然ログイン情報を含みます)。
またユーザが再度このページにアクセスした際ブラウザはHTTPプロトコルを通してローカルのcookieの内容をサーバに送信し、検証を行います。
または継続して前の操作を行います。

session

sessionとは、簡単に言えばサーバ上に保存されたユーザの操作の履歴情報です。
サーバはsession idを使用してsessionを識別します。
session idはサーバが生成します。ランダム性とユニーク性を保証し、ランダムな秘密鍵に相当します。
ハンドシェイクやデータ通信中にユーザの本当のパスワードが暴露されるのを防ぎます。
しかしこの方法では、依然としてリクエストを送信したクライアントとsessionを対応させる必要があります。そのためcookieメカニズムによってクライアントのID(session id)を取得することで、GETメソッドでidをサーバに送信することができます。

早速 cookieについて見ていきましょう。
お使いのブラウザで、 F12 を押して開発者用のコンソールが出せます。
Chrome ですと、Applicationタブに、Cookies というものがあるのが見えますので、それが保存されているCookie になります。
記載の通り、クライアント(ブラウザ)に保存されています。

GoでCookieを設定する

これは実は少し前のトークンの部分で使っていますね。
ですので、今回はコード例だけ見てみましょう。
net/http パッケージにある SetCookie でセットすることが可能です。

http.SetCookie(w ResponseWriter, cookie *Cookie)

Goでcookieを読む

今度は読む方法です。

cookie, _ := r.Cookie("username")
fmt.Fprint(w, cookie)

rはrequest ですね。

session

sessionはWeb開発環境ではまた新しい意味が含まれます。
クライアントサイドとサーバサイドの間でステートを保持するためのソリューションです。
しばしばSessionはこのようなソリューションの保存構造も指します。
sessionメカニズムはサーバサイドのメカニズムです。サーバでハッシュテーブルの構造に似たもの(ハッシュテーブルを使う場合もあります)を使用することで情報を保存します。

ということです。
Session は若干難しい話になっておりますが、簡単に言うとクライアント-サーバ間でデータを保持するためのものと捉えて良いと思います。

まとめの部分がかなりわかりやすいので、読んでみます。

上述の通り、sessionとcookieの目的は同じです。
どちらもhttpプロトコルのステートレスであるという欠点を克服するためにあります。
しかしその方法は異なります。sessionはcookieを通じてクライアントにsession idを保存します。
またユーザの他のセッション情報はサーバのsessionオブジェクトに保存されます。
これとは対照的に、cookieはすべての情報をクライアントに持たせる必要があります。

ということです。データの保存する部分がクライアントかサーバかということですね。
Webセキュリティにも関わってくる話ですが、クライアント側で保持ししているデータに関しては、そのまま使用するべきではなくサーバ側では検証をする必要があったりします。
なので、このあたりは気をつけたほうが良いと思います。

では次章 Goはどのようにしてsessionを使用するか をやります。

Goはどのようにしてsessionを使用するか

現在Goの標準パッケージにはsessionのサポートがありません。
この節では実際に手を動かしてgoバージョンのsession管理と作成を実現してみます。
session のサポートがないらしいので、ここで自作をするらしいです。

sessionの作成過程

サーバサイドで保持する必要があるので、作成部分をいくつかのステップに分けるようです。

  • グローバルでユニークなIDの生成(sessionid)
  • データの保存スペースを作成。普通はメモリの中に対応するデータ構造を作成します。しかしこのような状況では、システムは一旦電源が切れると、すべてのセッションデータが消失します。もしeコマースのようなホームページであった場合、これは重大な結果をもたらします。そのため、このような問題を解決するためにセッションデータをファイルの中やデータベースの中に書き込むことができます。当然この場合I/Oオーバーヘッドが増加しますが、ある程度のsessionの永続化は実現できますし、sessionの共有にも有利です。
  • sessionのグローバルでユニークなIDをクライアントサイドに送信します。

なるほど、当然どれも必要ですね。

session のユニークIDの伝え方を今度は考えると、下記の2つになるようです。

  • Cookie サーバサイドはSet-cookieヘッダーを設定することでsessionのIDをクライアントサイドに送信することができます。クライアントサイドは以降の各リクエストすべてにこのIDを含めます。またsession情報を含んだcookieの有効期限を0(セッションcookie)、つまりブラウザプロセスの有効期限に設定することもよく行われます。各ブラウザはそれぞれ異なる実装がされていますが、差はそれほど大きくはありません(一般的にはブラウザウィンドウを新規に作成した際に反映されます)。
  • URLの書き直し いわゆるURLの書き直しとは、ユーザに返されるページの中のすべてのURLの後ろにsessionIDを追加することです。このようにユーザがレスポンスを受け取った後、レスポンスのページの中のどのリンクをクリックしたりフォームを送信しても、すべて自動的にsessionIDが付与されます。これによりセッションの保持を実現します。このような方法はすこし面倒ではありますが、もしクライアントサイドがcookieを禁止している場合、このようなソリューションがまず選ばれます。

1番目は、cookiesession_id を含めて送信してもらうパターンです。
2番目は、URLについている、jsessionId=XXXXX とか見たことがあると思います。アレデスね。

Goでsession管理を実現する

session管理設計

session管理には下記が必要です。

  • グローバルなsessionマネージャ
  • sessionidがグローバルにユニークであることの保証
  • 各ユーザをひとつのsessionに関連付ける
  • sessionの保存(メモリ、ファイル、データベース等に保存できます)
  • sessionの期限切れ処理

これらを実装するようですね。
さてここからはコード例になるので、自分の環境で試してみます。
(前回と同様の環境を使います。)

<html>
<head>
<title></title>
</head>
<body>
count: {{.}}
</body>
</html>
view raw count.gtpl hosted with ❤ by GitHub
<html>
<head>
<title></title>
</head>
<body>
<form action="/login" method="post">
<input type="checkbox" name="interest" value="football">サッカー
<input type="checkbox" name="interest" value="basketball">バスケットボール
<input type="checkbox" name="interest" value="tennis">テニス
ユーザ名:<input type="text" name="username">
パスワード:<input type="password" name="password">
年齢:<input type="text" name="age">
<input type="hidden" name="token" value="{{.}}">
<input type="submit" value="ログイン">
</form>
</body>
</html>
view raw login.gtpl hosted with ❤ by GitHub
package main
import (
"github.com/astaxie/session"
_ "github.com/astaxie/session/providers/memory"
"net/http"
"html/template"
"log"
"fmt"
"time"
)
var globalSessions *session.Manager
var loginSession session.Session
func main() {
http.HandleFunc("/", index) //アクセスのルーティングを設定します
http.HandleFunc("/login", login) //アクセスのルーティングを設定します
http.HandleFunc("/count", count) //アクセスのルーティングを設定します
err := http.ListenAndServe(":9090", nil) //監視するポートを設定します
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
func init() {
globalSessions, _ = session.NewManager("memory","gosessionid",3600)
}
func login(w http.ResponseWriter, r *http.Request) {
loginSession = globalSessions.SessionStart(w, r)
r.ParseForm()
if r.Method == "GET" {
t, _ := template.ParseFiles("html/login.gtpl")
w.Header().Set("Content-Type", "text/html")
t.Execute(w, loginSession.Get("username"))
} else {
loginSession.Set("username", r.Form["username"])
http.Redirect(w, r, "/", 302)
}
}
func index(w http.ResponseWriter, r *http.Request) {
if loginSession != nil {
fmt.Println("sessionId: ", loginSession.SessionID)
fmt.Println("Username: ", loginSession.Get("username"))
} else {
fmt.Println("loginSession is nil")
}
fmt.Fprintf(w, "Hello astaxie!") //ここでwに書き込まれたものがクライアントに出力されます。
}
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) + 360) < (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("html/count.gtpl")
w.Header().Set("Content-Type", "text/html")
t.Execute(w, sess.Get("countnum"))
}
view raw session.go hosted with ❤ by GitHub
https://gist.github.com/katsuobushiFPGA/c086076486a223cafd112bd3abf29779

Sessionのサンプルプログラムを書きました。
Session に関しては、筆者の方のライブラリを使用させていただいております。
(この章のsesson の制作をライブラリ化したもの)

2つほど動作を確認できるものがあります。

  • /count にアクセスした際にアクセス数がカウントアップされることのテスト
  • /login で 入力したユーザ名を リダイレクトした後もサーバ側で保持できているかのテスト

上記が確認できますので試してみてもらえればと思います。

sample-01

sample-02
sample-03

Session のサンプルが試せたと思います。

sessionストレージ

こちらはコード例のみで、上記と同じような内容になりますので、じっくり読むことで理解します。
この章はこれで終わりにします。

sessionハイジャックの予防

この章は、sessionハイジャック と呼ばれる 中間者攻撃についての理解を深めるパートです。

sessionハイジャックは広範囲に存在する比較的重大な脆弱性です。
session技術において、クライアントサイドとサーバサイドはsessionのIDによってセッションを維持します。
しかしこのIDは簡単にスニッフィングされ、第三者に利用されてしまいます。これは中間者攻撃の一種です。

先程、コンソールに出力していたsessionID ですね。
これが盗まれて、第三者に利用されたときにどうするべきかというものです。

sessionハイジャックの過程

先程、session のサンプルを書いたものと同じものを使用していきます。
count.gtpl も同じでいいですね。

cookie の情報を確認してみます。
gosessionid という 今回管理している cookievalue を確認してみます。
session-01

次のステップが重要です:別のブラウザ(ここではfirefoxブラウザを開きました)を開き、
chromeのアドレスバーのアドレスを新たに開いたブラウザのアドレスバーにコピーします。
その後firefoxのcookieエミュレートプラグインを開き、新規にcookieを作成します。
上の図のcookieの内容をそのままfirefoxの中に再設定します

やってみましょう。

Firefoxで localhost:9090/count にアクセスしてみると、最初は Count:1 が表示されています。
session-02
同じものにしてみました、保存してみましょう。

F5で更新してみると、 Firefoxの方は count:10となりました!
Firefox と Chromeのキャプチャです。
session-03

ブラウザを変えても、sessionIDを取得することができました。
この後cookieの保存過程をエミュレートします。
この例は一台のコンピュータの上で行ったものです。
たとえ二台によって行ったとしても結果は同じです。この時もし交代で2つのブラウザのリンクをクリックした場合、操作しているカウンターが実は同じものであるということに気づくでしょう。

おぉ~。sessionID を盗んで同じセッションになりましたね。
これがログイン後の画面のセッションとかですと、他の人のログインセッションを乗っ取ることができるわけですね。

ここではfirefoxがchromeとgoserver間のセッション維持の鍵を盗みました。
すなわち、gosessionidです。これは"セッションハイジャック"の一種です。
goserverからすると、httpリクエストからgosessionidを得ました。
HTTPプロトコルのステートレスによってgosessionidがchromeから"ハイジャック"されたものなんか知る方法はありません。
依然として対応するsessionを探し、関連する計算を実行します。
同時にchromeも自分が保持しているセッションがすでに"ハイジャック"されたことを知る方法もありません。

なるほど、セッションIDは鍵なので鍵が盗まれたという感じですね。

sessionハイジャックの予防措置

cookieonlyとtoken

この節は予防方法についてのようです。

  • ひとつの方法はsessionIDの値にcookieによってのみ設定されるようにすることです。URLの書き直し方法は許さないようにし、同時にcookieのhttponlyをtrueに設定します。

なるほど?

このプロパティはクライアントサイドのスクリプトが設定されたcookieにアクセスできるか否かを設定します。まず、これによってこのcookieがXSSによって読み取られ、sessionハイジャックを引き起こすことを防止できます。

HttpOnlyJavascript からの読み込みができなくなるので、上記のようなケースを防げるわけですね。

つぎにcookieの設定がURLの書き直し方法によって容易にsessionIDを取得することができなくなります。

なるほど

  • ステップ2は各リクエストの中にtokenを追加することです。前の章で述べたformの重複送信を防止するのに似た機能を実装します。各リクエストの中で隠されたtokenを追加し、毎回このtokenを検証することでユーザのリクエストがユニークであることを保証します。

これはフォームの章でもやりましたね。
token を用意しておくことで個々のクライアントを識別することができます。

間隔をおいて新しいSIDを生成する

もう一つの方法は、sessionの他に作成時間を設けることです。
一定の時間が過ぎると、このsessionIDは破棄され、再度新しいsessionが生成されます。
このようにすることで、ある程度sessionハイジャックの問題を防ぐことができます。

なるほどね。

sessionが始まると、生成されたsessionIDの時間を記録する一つの値が設定されます。
毎回のリクエストが有効期限(ここでは60秒と設定しています)を超えていないか判断し、定期的に新しいIDを生成します。これにより攻撃者は有効なsessionIDを取得する機会を大きく失います。
上の2つの手段を組み合わせると実践においてsessionハイジャックのリスクを取り除くことができます。
sessionIDを頻繁に変えると攻撃者に有効なsessionIDを取得する機会を失わせます。
sessionIDはcookieの中でやりとりされ、httponlyを設定されるため、URLに基づいた攻撃の可能性はゼロです。同時にXSSによるsessionIDの取得も不可能です。
最後にMaxAge=0を設定します。これによりsession cookieがブラウザのログの中に記録されなくなります。

結構大変そうですが、セキュリティ対策と考えれば必須の処理ですね。

おわりに

今回はsession について勉強しました。
go での session 管理は結構大変そうです。
セキュリティ対策については、すべての言語で共通ですので、知っておかないといけないことですね。
次からは、テキスト処理 についてです。

Hugo で構築されています。
テーマ StackJimmy によって設計されています。