Cookieとかセッションとかセキュリティとか


概要

社内勉強会でcookieの仕様(主にRFC6265)やセキュリティ(主にCSRFやセッション乗っ取り)について話した内容をまとめます。

但し書き

手っ取り早い説明用に内容を端折ってまとめているため、厳密には正しくない記述が含まれます。
ちゃんと知りたければRFC読みましょう。(幸い日本語訳があってかなり読みやすいのでオススメ)

以下、RFCに沿って「サーバー」と「UA」という単語を使いますが、UAはブラウザと言い換えていいと思います。
curlやプログラマブルなクライアントは、この枠外の挙動をさせることも容易なので対象外です。

またブラウザ毎の実装については、過去の他の方の記事を参照していて自分で試せてはいないため、現在では実装が異なっている可能性があります。

cookieの仕様

cookie(webでの状態管理メカニズム)についての仕様。

cookieの概要

動作イメージ(https://triple-underscore.github.io/RFC6265-ja.html)

== サーバ → UA ==

Set-Cookie: SID=31d4d96e407aad42; Path=/; Domain=example.com

== UA → サーバ ==

Cookie: SID=31d4d96e407aad42

サーバーがHTTP(S) Responseに Set-Cookieというヘッダで情報を送ると、UAは後続のHTTP(S) Requestにおいて、Cookieというヘッダにその情報を載せてサーバーに返す。

ただし、UAはSet-Cookieを無視してもいいし、任意のタイミングで内容を破棄してもいい。

cookieの属性

属性は、UA側でのCookieの管理方法を規定するためにあります。

サーバーからのSet-Cookieの情報の一部として付与されるので、UAはそれに従います。
(UA側からのリクエスト時には付与されない)

(※以下、意訳。各項目の詳細はRFC参照のこと。)

  • Path
    • 異なるパスで送るCookieを制御できるため、pathによってサービスが違う場合に使える
    • ただし、異なるパスへのCookie付与はできてしまうため、完全性は保証されない。
  • Domain
    • 指定すると、下位ドメインにまたがってCookieを扱える。
    • サブドメインと共有したい意図がなければ指定しないべき
  • Secure
    • 一般にはSSL通信専用として扱われる。
    • セッション情報を管理するなら必須
  • Expires
    • 指定しなければ、セッション(定義はブラウザ次第)が終われば破棄される
  • Max-Age
    • ↑とほぼ同じ。定義の仕方が異なる程度
  • HttpOnly
    • jsなどで操作することを許可しない
    • XSSなどで悪意のあるスクリプトによって値が読み取られることがない

Cookieは「セッションのためのトークンを保持する」という目的で扱われることが多いので、適宜設定しておく必要があります。

ちなみにplay2のデフォルトはこのような形です。

(動作確認のためかSecureがfalseになっていますが、本番ではtrueにすべき。)

cookieのブラウザ毎の差異

Path 属性

Netscapeの仕様とRFC6265において、pathの仕様が異なり、古いブラウザ(主にIE)では互換性のためNetscapeの仕様を維持しています。

  • RFC6265

'/foo' というパスのクッキーがあった場合、"/foo", "/foo/", "/foo/bar" といった url のケースではクッキーが送信されるが、"/foobar" などには送信されない。

  • Netscape

単純に前方一致でのみ判定する。"/foo" というパスのクッキーは "/foobar", "/foo/bar" にマッチすべきとう例まであげている。

私見としては特に必要がなければ(異なるパスで独立のサービスを運用しているなど)、パス指定しないのが無難かと。

domain 属性

仕様には以下のように書いてある。

一部の既存の UA は、 Domain 属性が不在であっても, Domain 属性が存在していて,現在のホスト名を含んでいるかのように扱う。 例えば, example.com が Domain 属性の無い Set-Cookie ヘッダを返した場合、これらの UA は,そのクッキーを www.example.com にも誤って送信することになる。

徳丸さんの記事ではこのようにまとめられていました。

実際のブラウザで調査したところ、Domain属性のないCookieの挙動は以下の結果となりました。
IE9 サブドメインにも送信される
Firefox 7.0.1 RFC6265通り
Google Chrome 14.0.835.202 RFC6265通り
Safari 5.1 RFC6265通り
Opera 11.51 RFC6265通り
iモード(P-07A) サブドメインにも送信される
Android 2.3.3 RFC6265通り

ところで、「サブドメインにも送信される」というときに、「co.jp」などのpublic suffixにcookieを付与したらどうなるのか、という話があります。
RFCでは、「正準化」の中で、事前に定義されているpublic suffix(あるいは通信先と異なるドメイン)についてはSet-Cookieは無視されるようです。

request-host を正準化した結果が domain-attribute に ドメイン合致 しないならば:
この手続きを中止する(クッキーをまるごと無視する)。

セキュリティ上の理由から、多くの UA では, public suffix に対応する Domain 属性は却下するように設定されている。 例えば、一部の UA は "com" や "co.uk" などの Domain 属性を却下することになる。

しかし、古いIEなどではpublic suffixでcookieを付与できてしまう不具合(クッキーモンスターバグ)があるようです。

クッキーモンスターバグがある条件では、トークンを用いたCSRF対策をしていても、「IPアドレスを偽装したなりすまし犯行予告」は防げないことになります。その条件とは、主に以下の両方が成立する場合です。
・地域型JPドメイン名または都道府県型JPドメイン名上にサイトがある
・利用者がIEを使っている

ちなみに、自分がホストするページに信用できないjsが入っている場合でも、任意のcookieを埋めることができてしまうので注意が必要です。

1st party cookie と 3rd party cookie

(この話題はWeb広告業界特有の話かと思います。)

ブラウザは3rd partyのSet-Cookieヘッダを無視できると仕様に書かれています。

ここでいう3rd partyとは、「利用者が直接­訪問しているドメインではないドメイン」のこと。
例えば、example.comのページにアクセスした際ににtracking.comの画像コンテンツが参照されていれば、それのhttp responseに付いてきたSet-Cookieヘッダを無視できます。

ブラウザ毎の3rd party cookiesの受け入れ状況はこんな感じです。

動作確認

akka-httpで作ってみたサンプルです。
コンテンツサーバをgithub-pages、トラッキングサーバをローカルのakka-httpとしています。

動作準備

コードをcloneしてきて、

sbt
trackingServer/run

するだけです。
コンソールでEnterを押すと停止します。

3rd-partyの場合

(予め、localhostのCookieを消しておきます)

  1. http://uryyyyyyy.shake-freek.com/cookieTrackingSample/static/3rd_party_tracking.html にアクセス
  2. localhostのトラッキングピクセルを読むときにSet-Cookieが送られる。
  3. 次回以降のアクセスの時にCookieが送られる。
  4. サーバーは受け取ったら標準出力で内容を確認する。
  5. ブラウザでスーパーリロードしてもう一度サーバーの出力を確認する。

Chromeなど3rd-party cookieを許可していれば、同じIDが返ってくるはずです。
Safariなど許可してないブラウザでは、毎回新しいIDが返ってくるはずです。

1st-partyの場合

(予め、uryyyyyyy.shake-freek.comのCookieを消しておく)

  1. http://uryyyyyyy.shake-freek.com/cookieTrackingSample/static/1st_party_tracking.html にアクセス
  2. localhostのサーバーからのjsが読まれてcookieを作る。
  3. jsがサーバーへTrackingIDを「cookieではない形式で」サーバーに送る
  4. サーバーは受け取ったら標準出力で内容を確認する。
  5. スーパーリロードしても同じIDでトラッキングできている。

こちらはcookieが有効であればどのブラウザでも同じIDが返ってくるはずです。

セキュリティの話

RFCでセキュリティの項に書かれているものを取り上げて補足していきます。
特性上、cookieだけじゃなく色々混ざっています。。

Ambient 権限

主にCSRFなどの問題に該当します。

3rd partyとの通信において、

  • Set-Cookieヘッダを無視したり、
  • Access-Control-Allow-Originヘッダを見て、レスポンスをブラウザ側で破棄する、

といったことはブラウザでは既に対応されていますが、Cookieを載せたリクエスト自体はできてしまいます。
(SNS系のwidgetとかそれで動いているのでは)

この時、サーバー側ではそのリクエストが正規のものなのか不正にコールされたものなのか判別できないという問題があります。
これはcookieの仕様なので、アプリ側で対応する必要があります。

pre flightの仕組み

ちなみに、ブラウザはページのドメインとは異なるページへのajaxリクエストの際に pre flightという仕組みがあります。
pre flightが飛ぶ条件は

[クロスサイトリクエストフォージェリ対策]によると(https://www.playframework.com/documentation/ja/2.3.x/ScalaCsrf)

・ PUT や DELETE のようなリクエストを使うようブラウザに強制する
・ application/json のようなコンテントタイプを送信するようブラウザに強制する
・ サーバが既に設定したものとは異なる、新しいクッキーを送信するようブラウザに強制する
・ ブラウザがリクエストに追加する通常のヘッダとは異なる、任意のヘッダを設定するようブラウザに強制する

の場合です。
(application/jsonのようなとありますが、application/x-www-form-urlencoded, multipart/form-data、text/plainの場合はpre flightが飛びません。)

この場合、XMLHttpRequestの中でpre flightリクエストでサーバー側にやりとりしていいかの確認を行い、承認が得られなければリクエストを送りません。
(pre flightではbodyもないしhttp methodがOptionになるため、普通は悪影響はないはずです。)

参考:
独自ヘッダをチェックするだけのステートレスなCSRF対策は有効なのか?

動作サンプル

Readmeに書いてあるとおりです。ここでは割愛します。

セッション識別子

セッション固定攻撃

ログイン前に識別子をつけておいて、ログイン後もその識別子で区別する実装の場合に該当します。
(普通はしないと思うんですが、PHPなどではそのような実装があるとかないとか)

その場合にセッションを乗っ取られる恐れがあります。

ワンタイムトークン(number used once = nonce)

慎重なサービスでは使って良いかもですが、複数のセッションが並行して走るときに管理がつらそうです。あまり深追いできてないです。

(play2-authでは、もちろんセッション成立時にIDを発行して、以降は同じIDを使い回す形)
https://github.com/t2v/play2-auth/blob/master/module/src/main/scala/jp/t2v/lab/play2/auth/AsyncAuth.scala#L30

完全性

サブドメイン、あるいは異なるパス、あるいは非HTTPS通信からのレスポンスを偽装されうる場合、任意のCookieを付与することはできてしまいます。
このあたりを考慮して、Cookieの情報は全て改ざんされても良いように設計する必要があります
(例:cookieの値がscriptとして評価されうる場合はXSS脆弱性になる。)

・Cookieはポートやプロトコル(http/https)をまたがって共有されている
・Cookieのsecure属性は平文でCookieを送信しないという設定であり、Cookieをセットする(受信する)場合には効果がない
・既にsecure属性つきCookieがあっても、HTTPのsecure属性なしCookieで上書きされる(IE10、Google Chrome、Firefoxで確認)

(乗っ取りは出来ないけど、攻撃者のアカウントからの操作のように見せることはできるのかも。)

あるいは、多数のクッキーを格納させることによりクッキーの削除をUAに強制する、という攻撃も可能です。

DNSへの依存

Webの仕組みなので、DNSが機能しなくなったら崩壊します。

Cookie以外でセッションを維持する方法

localStorageに置いておいて、jsでその値を読んでリクエストに含める。

  • 良い点
    • 他ドメインのlocalStorageは見れないのでCSRFなどの問題がなさそう
  • 悪い点
    • CORSができなくなると思う
    • 対象ドメインへのアクセスで、かつ事前にjsが読まれていることが必要
    • CookieのhttpOnlyのような属性を付けられず、ドメインページに含まれるjsからは内容が読めてしまう。

その他、細かいけど気になるところ

  • 同一ホストの異なるポート間ではCookieは共有される
  • Hostを指定すると、サブドメインへもCookieは共有される。
  • UA は、クッキー保管庫に 失効した クッキーが存在する場合は,いつでも、それら 失効した クッキーすべてをクッキー保管庫から抹消しなければならない。
  • UA は HTTP リクエストを生成する際に、複数の Cookie ヘッダを添付してはならない。

  • UAのCookie保持の制限(あくまで参考値でUAの実装次第)

    • 1クッキーあたり,少なくとも 4096 バイト(クッキーの名前, 値, 属性 の長さの総和で)
    • 1ドメインあたり,少なくとも 50 個のクッキー
    • 全部で少なくとも 3000 個のクッキー
  • サーバは、同じ応答­内に同じ cookie-name の複数の Set-Cookie ヘッダを内包するべきでない。

    • (Set-Cookie ヘッダが複数あるのは許容されてるっぽい。)
  • 典型的なセッション識別子の期限は,2週間 程度が妥当