セッションクッキーの扱いを変えるときはドメインの設定に気をつけよう

2 min read読了の目安(約2300字

利用してたもの

  • PHP(Phalconですが、FW関係なく起こりえます)

概要

先日、既存のセッションクッキーにsecure属性だったり、httponly属性がついてないことに気づきまして既存実装を修正しようとしていました。

FWの機能ではセッションに属性をつけることができないことがわかり、session_set_cookie_params関数を利用してデータを入れようとしました。
その際、私の認識が甘かったこともあり、ドメインにHttpHost名を入れました。かつ、lifetimeの値にsetcookie関数と同様にtime()を足してしまいました。(その結果有効期限が2071年くらいになる)

何が起きたか

元々あったdomain.localhostのセッションクッキーと.domain.localhostのセッションクッキーがブラウザ内にできました。普通はできないはずだと思うのですが、色々試してたところ、できてしまいました。(一応同じ状況を作るなら自分で勝手にsetcookieしちゃえば同じ状況を作れます)

これは良くないと思ったので、session_set_cookie_params関数のdomain属性を空値に変更をしました。(同時にtime()の部分も削除しました)

そうするとですね、ブラウザが送ってくる値が.domain.localhostのセッションIdになってしまいました。(どうやら古いものを優先的に送る?←違いました)

問題になったこと

表示の際にはセッションに存在しているcsrfのトークンの値が、送信時にはサーバー側にはセッションにcsrfトークンのキーが別の値になっていることがわかりました。POST時にどうやら変わってしまってる様子です。

もっと調べてみたところ、Request HeaderのCookieに同じSessionキーのもので、値が異なるものが2つ送られていました。
Cookieに関して、色々調べてみると以下のサイトにあたりました。

https://www.yunabe.jp/docs/cookie_and_security.html

Cookie ヘッダでブラウザから送信されるクッキーの順序は以下のように決まります。

  • 後述する path 属性のなるべく長いものが前に来る
  • path 属性の長さが同じ場合は、最近作られたものが前に来る

また後述するように、クッキーは name が同一でも domain, path 属性の値が異なれば別のものとして扱われるので、同じ name のクッキーが複数 Cookie ヘッダの中に現れることがありうるので注意してください。

ということでですね、.domain.localhostのほうのセッションキーのほうが先に送られることになります。PHPは後にある同じセッションキーを見ないようで、先にある.domain.localhostのセッション値を読もうとしてしまいます。

結果起きてしまうこと

.domain.localhostの方のセッション値を読んでしまうことがわかりました。
そこで、セッション自体をサーバーから削除してみると、問題が広がります。

今回、domainの値を戻したのが原因が広まってしまったのですが

  1. 優先されて来るのは.domain.localhostのセッション値
  2. アプリケーションは存在しないことが分かったので、セッションを新しく作ってブラウザにdomain.localhostのセッションクッキーを作らせる
  3. 作らせるものの、.domain.localhostのほうが文字数が1文字長いので優先されるのは.domain.localhostになります。
  4. 2に戻る

このようになってしまう結果、csrfのトークンの検証ができなくなったということですね。

また、作りによってはですが、ログインページ等でログインしていたらマイページに飛ばしますという処理を書いていた場合、ログインページに来たときには自動的にマイページに飛ばそうとするが、セッションがdomain.localhostの方に登録されるので.domain.localhostには存在しないからまたログインページに戻ってくるといった無限ループに繋がります。

ここでですね、.domain.localhostのセッションクッキーの期限がブラウザセッションになっていたりすればブラウザの再起動で直せたのでしょうけど、最初に書いたとおり、lifetimeを設定しようとして間違えてtime()関数を入れてしまったことがよりこの問題を複雑化してしまうことになりました。

setcookieでの削除も考えましたが、先程の流れの通り、2でSetCookieで作られるからかsetcookieでの削除はできない感じでした。(もしかしたらやり方があるのかもしれません。header関数だとreplaceをfalseにしたら行ける気がしたんですが、なぜかうまく行きませんでした)

結果

おそらくこれを対処するためには、セッションキーをそもそも変えてしまうしか無いような気がしました。
この問題の厄介なところは、すぐには原因がわからないことです。(元のcookieが残っていればなんとかなったりする)
今回は本番に反映してないので影響はなかったのですが、cookieの扱いには気をつけましょう。。。(ドメインは意図が無い限りは空値にしようね!)