PHPのSession Adoptionは重大な脅威ではない

なぜPHPアプリにセキュリティホールが多いのか?:第25回 PHPのアキレス腱にて、大垣靖男氏がPHPのSession Adoption問題について取り上げている。大垣氏は度々この問題を取り上げているが、今のところ氏の主張に同調する人を見かけない。それもそのはずで、大垣氏の主張は間違っていると私は思う。
 以下、大垣氏の主張を実際に試してみる形で、順に説明しよう。

大垣氏の主張

 大垣氏の主張は、PHPにはSession Adoption脆弱性があるために、標準的なSession Fixation対策であるsession_regenerate_id()を施しても、その対策は有効ではないというものだ。

しかし,実際には現在に至るまでPHPのセッションモジュールのセッションアダプション脆弱性は修正されないままになっています。このために,本来はsession_regenerate_id関数をログイン処理を行う前に呼び出すだけでよいはずのログイン処理に,脆弱性に対応するための余計な手間が必要となっています。

第25回 PHPのアキレス腱 ── セッション管理

 読者の便宜のために氏の主張の要点をまとめると以下のようになる。

  • 一般的にSession Fixation対策にはセッションIDの振りなおしが有効とされる
  • PHPでは上記はsession_regenerate_id()関数により実施できる
  • しかし、PHPにはSession Adoption脆弱性があるのでこの対策だけでは不十分
  • その理由は、同一名称で複数のCookieを指定できるため

 ということらしい。問題は後半の二つなので以下検証する。

Cookie Monsterバグ

 まず、同一名称で複数のCookieが指定できることは、Cookieの仕様だと思う。よく知られているように、Cookie指定の際には、domainとpathの属性を指定できる。これらが異なっていれば、同一名称のCookieが指定できる。


例:
Set-Cookie: HOGE=123; domain=hash-c.co.jp
Set-Cookie: HOGE=456; domain=www.hash-c.co.jp

 同一名称のCookieが複数できるとややこしいが、Cookieの発行はサイト運営者の責任であるので、運営者が適切に管理している限り問題ない。しかし、以下のような場合には、問題が発生する。

  • 同一ドメインにある複数ホストの中に脆弱性があるものがあると、他のサイトまで影響を受ける可能性がある
  • Cookie Monsterバグにより、Cookie値を不正にセットされる可能性がある

 大垣氏の説明では、一番目のパターン(同一ドメインのサイトの中に脆弱性があるサイトがある)で説明しているが、ここでは2番目のパターンを例に説明しよう。Cookie Monsterバグとは、ブラウザによっては、例えば .co.jp ドメインCookieが設定できるという問題だ。過去のNetscape/Mozilla、現在のSafariにはこの問題がある*1。すなわち、以下のようなCookie発行が有効である。


Set-Cookie: PHPSESSID=jtcok9spkmvusm67cvo8us0df2; domain=.co.jp

 これは結構凶悪な問題で、すべての.co.jpドメインに有効なPHPSESSIDという名前のCookieが設定できてしまう。しかし、これはブラウザの問題であって、PHPの問題ではない。
 これにSession Adoption脆弱性が加わると以下のような指定も有効となる。


Set-Cookie: PHPSESSID=AAA; domain=.co.jp

 AAAは適当に作成したセッションIDだが、PHPはそのような「オレオレ・セッションID」も受け入れてくれる。このCookieは、「.co.jpドメイン上のすべてのPHPアプリケーションで有効なPHPセッションID」として作用する。
 これにより、Session Fixation攻撃が容易になるという側面は確かにあるが、この問題がなくとも、攻撃対象のサイトを閲覧して有効なセッションIDを取得することでSession Fixation攻撃は可能だ。このあたりは、とくまるひろしのSession Fixation攻撃入門を参照いただきたい。

Cookie Monsterバグを利用したSession Fixationのデモ

 ここで、非常に簡単なSession Fixationのデモをお見せしよう。
 サンプルサイトは、top.php、login.php、profile.phpの三画面からなる。top.phpでユーザIDを入力し、login.phpが認証、profile.phpで個人情報を表示する。簡単のため、パスワードは入力しない、認証は必ず成功する、個人情報はユーザIDのみ表示となっている。以下、ex.phpで定義された関数ex()は、HTMLエスケープして表示するための関数である。

http://example.co.jp/top.php:

<?php
  require_once 'ex.php';
  session_start();
?>
<html>
<body>
セッションID:<?php ex(session_id()); ?><BR>
<form action="login.php" method="POST">
ユーザIDを入力:<input name="id"><br>
<input type="submit">
</form>
</body>
</html>

http://example.co.jp/login.php:

<?php
  require_once 'ex.php';
  session_start();
  $id = $_POST['id'];
  $_SESSION['id'] = $id;
?>
<html>
<body>
セッションID:<?php ex(session_id()); ?><BR>
<?php ex($id); ?>さん、ログイン成功です<BR>
<a href="profile.php">個人情報</a>
</body>
</html>

http://example.co.jp/profile.php:

<?php
  require_once 'ex.php';
  session_start();
?>
<html>
<body>
セッションID:<?php ex(session_id()); ?><BR>
ユーザID:<?php ex($_SESSION['id']); ?><BR>
</body>
</html>

 ここに、攻撃者がhogehoge.co.jpドメイン上に以下のようなワナを仕掛けたとする。

http://hogehoge.co.jp/wana.html:

<html>
<head>
<META http-equiv="Set-Cookie" content="PHPSESSID=AAA; path=/; domain=.co.jp">
</head>
<body>
<a href="http://example.co.jp/top.php">example.co.jp</a>で楽しいキャンペーン開催中。
いますぐログインしてグッズをゲットしよう
</body>

 example.co.jpの正規ユーザがうっかり、wana.htmlをSafariブラウザで閲覧すると、META要素により、.co.jpドメインのPHPSESSIDがセットされる。以下、以下のような流れとなる。

ワナのサイト。

.co.jpドメインのPHPSESSIDがセットされる。リンクにより、example.co.jpに遷移。

ユーザIDの入力

ログイン成功。PHPSESSIDはAAAとなっている。

個人情報表示

 ここでワナを仕掛けた人は、被害者のPHPSESSIDがAAAになっていることを知っているので、適当なタイミングで自分もPHPSESSID=AAAをセットしてhttp://example.co.jp/profile.phpを閲覧することにより、個人情報を入手することができる。これがCookie Monsterを使ったSession Fixationの流れである。 この例では、Session Adoptionも使っているが、PHP以外の言語で作成されたサイトを攻撃する場合は、当該サイト(この例ではexample.co.jp)を閲覧して得られた正規のセッションIDを利用して攻撃を行う。その場合も「攻撃者はセッションIDを知っている」ことには変わりなく、その後の手順はまったく同じだ。

session_regenerate_id()による対策

 次に、Session Fixationに対する標準的な対策であるsession_regenerate_id()を呼び出すサンプルを示そう。これは、login.phpに以下のようにsession_regenerate_id()を挿入するだけだ。参考のために新旧のセッションIDを表示するようにした。

http://example.co.jp/login.php:

<?php
  require_once 'ex.php';
  session_start();
  $sid1 = session_id(); // 旧セッションIDの待避
  session_regenerate_id(); // 追加

  $id = $_POST['id'];
  $_SESSION['id'] = $id;
?>
<html>
<body>
旧セッションID:<?php ex($sid1); ?><BR>
現セッションID:<?php ex(session_id()); ?><BR>
<?php ex($id); ?>さん、ログイン成功です<BR>
<a href="profile.php">個人情報</a></body>
</html>

login.phpの実行結果は以下のようになる。

そして、肝心の個人情報閲覧画面だが、以下のように、.co.jpドメインのセッションIDの方が有効となっており、有効な情報は閲覧できない。

Cookie Monsterバグの影響

 この状態で、Webサーバーへのリクエストをキャプチャしてみると、以下のようなCookieが送出されていることがわかる。

Cookie: PHPSESSID=AAA; PHPSESSID=89c21khagvg1iiq682pg2meq03

すなわち、.co.jpドメインのPHPSESSID=AAAと、example.co.jpドメインのPHPSESSID=89c21khagvg1iiq682pg2meq03の両方が送出されている。この例では、たまたまAAAの方が有効となり、個人情報は表示されなかったのだ。
試みに、Cookieの順序を入れ替え、AAAを後回しにすると、表示は以下のようになる。


結局Session Adoptionは問題ではない

 上記のメカニズムを説明しよう。session_regenerate_id()が呼ばれた時点で、「有効なセッションID」は、89c21khagvg1iiq682pg2meq03に変わっているのだ。従って、89c2...を送出しない限りセッションは有効にならない。そして、89c2...は攻撃者の知り得ないセッションIDである。すなわち、Session Fixationは成立しない。
 ユーザにとっての不利益は、.co.jpドメインCookieが有効な場合にアプリケーションが正しく動かないことだ。しかし、これはSafariCookie Monsterバグの影響であって、同じ問題はSession Adoptionがなくても発生する。すなわち、PHPには責任はまったくない。

大垣氏はどこで間違えたか

 (Cookie Monsterバグを含む)同名のCookieとSession Adoptionの関係について、大垣氏は以下のように説明している。 

wiki.ohgaki.net以外のwww.ohgaki.netとblog.ohgaki.netで利用しているブログアプリが session_regenerate_id関数を呼んでセッションIDを再生成していても,ohgaki.netに設定されたクッキーが優先されてしまうため,新しいセッションIDは利用されず,攻撃者にとって既知のセッションIDでセッションを初期化してしまうからです。

http://gihyo.jp/dev/serial/01/php-security/0025?page=2

 「ohgaki.netに設定されたクッキーが優先されてしまう」というのは、ブラウザから送られてくるリクエストの話だ。PHPアプリケーションの内部では正しくセッションIDを再生成している。前述のように「攻撃者にとって既知のセッションID」では、アプリケーションの処理を継続できない。だから、「攻撃者にとって既知のセッションIDでセッションを初期化してしまう」わけではない。
 大垣氏はたびたびPHPのSession Adoption問題を提起されているが、PoC(Proof of Concept; 概念実証)のコードを一度でも試したのだろうか。おそらく試していないのだろう。PoCを一度でも書いてみれば、大垣氏の懸念は心配にあたらないことがすぐに分かるからだ。もしPoCがあるのであれば、見せていただきたい。

まとめ

 Safari3のCookie Monsterバグとの組み合わせを題材として、Session Adoption問題を検証した。以上に説明したように、Session Adoption問題は、新たな脅威には影響しない。Session Fixation脆弱性は、Session Adoption問題の有無に関係なくsession_regenerate_id()により有効に対策されるし、Cookie Monsterバグはブラウザ(Safariなど)単体の問題だ。Appleのサイトでは既にSafari4βのダウンロードが標準になっているので、SafariCookie Monster問題が解消されるのも時間の問題だろう。
 従って、Session Adoptionはない方がよいとは私も思うが、重大なセキュリティ脅威のようにとらえるのは間違っている。

*1:安定バージョンの最新版Safari3.2.3で確認。Safari 4βでは改修されている模様だ