2013年10月21日月曜日

PHPのsetcookie関数で空文字列を設定しようとするとクッキーが削除される

PHPでスクリプトを書いていて、setcookieの第2パラメータ(クッキーの値)の変数をタイプミスしたところ、以下のレスポンスヘッダが送信されていました。
setcookie('A', $misspelled_variable);
 ↓ 結果
Set-Cookie: A=deleted; expires=Thu, 01-Jan-1970 00:00:01 GMT
日付が大昔になっているし、クッキーの値に「deleted」は指定していません。これは、クッキーを削除する時の書き方ですが、PHPでクッキーの削除というと、expiresに過去日付を明示する方法をよく見かけますが、単に第2パラメータを空文字列にすればよかったのか…と思いマニュアルを見たら、一応書いてありました。
http://php.net/manual/ja/function.setcookie.php
陥りやすい失敗
クッキーは設定されたものと同じパラメータで削除する必要があります。 値が空文字列あるいは FALSE で、その他の全ての引数が前に setcookie をコールした時と同じである場合に、指定された名前のクッキーが リモートクライアント上から削除されます。 内部的な動作として、これは値を 'deleted' に変更した上で有効期限を 1 年前に設定しています。
「一応」と保留したのは、マニュアルの本文ではなく「陥りやすい失敗」という項に書いてあったからです。それに、マニュアルには「1年前」とありますが、実際に設定される日付はずっと古い日付です。
そう思って、PHP5.2.6で試したら、以下のように、1年前の日付になります。
Set-Cookie: A=deleted; expires=Sat, 20-Oct-2012 07:17:38 GMT
どこかで仕様変更されたのだろうと思い、バイナリサーチで調べたところ、PHP5.3.6までは「1年前」、PHP5.3.7以降が1970年1月1日に変更されていました。

ソース上で確認すると、ext/standard/head.cのphp_setcookie関数で以下のように修正されていました。
// PHP5.3.6
if (value && value_len == 0) {
  time_t t = time(NULL) - 31536001;
  dt = php_format_date("D, d-M-Y H:i:s T", sizeof("D, d-M-Y H:i:s T")-1, t, 0 TSRMLS_CC);
  snprintf(cookie, len + 100, "Set-Cookie: %s=deleted; expires=%s", name, dt);
  efree(dt);
} else {


// PHP5.3.7
if (value && value_len == 0) {
  dt = php_format_date("D, d-M-Y H:i:s T", sizeof("D, d-M-Y H:i:s T")-1, 1, 0 TSRMLS_CC);
  snprintf(cookie, len + 100, "Set-Cookie: %s=deleted; expires=%s", name, dt);
  efree(dt);
} else {
この変更はChangeLogにも載っていないので変な感じですが、クッキーを削除したい場合はexpiresをできるだけ古い時刻にするという考え方は妥当です。そうしないと、端末の時計が狂っている場合に、クッキーが削除されずに、「PHPSESSID=deleted」という形に設定される危険性が高くなります。これが複数ユーザ存在すると、一種のセッション固定の状態になります。PHP5.5.2以降で導入されたstrict sessionsを設定していれば大丈夫ですが、まだstrict sessionsを設定しているサイトはごくわずかでしょう。

時計をあわせていない利用者の方が悪いという方もいるでしょうが、例えばこちらの事例のように、携帯機器のブラウザが「4年前の日付」になっている例もあるので、1年前くらいでは確かに不安です。「1時間前」の時刻をセットしてクッキーを削除する例をよく見ますが、端末の時計が狂っている前提では、意図に反して脆弱になる可能性があります。

私は「ログアウト時にセッションクッキーを削除する必要はない」という意見を持っています(参考:「ログアウト機能の目的と実現方法」)が、仮に削除するのであれば、端末の時刻が狂っている場合を想定して、削除時のクッキーの値には暗号論的に安全な乱数(openssl_random_pseudo_bytes関数か、/dev/urandomが利用できます)を設定しておくのがよいと思います。そうすれば、万一クッキーが削除されず設定された場合でも、脆弱性にはなりません。

0 件のコメント:

コメントを投稿

フォロワー