クッキー / セッション
更新日:2024年03月27日
作成日:2016年02月21日
セッションの利用
変数はページがリロードされると、その際にクリアされてしまいますが、セッションを利用することでセッション変数の中に入れたデータは、ブラウザのページを更新しても、別のページへ移動しても保持することができます。
セッションとは、ユーザーがページを移動している間、そのユーザーのアクセス情報をそのユーザーごとに保持したまま移動できる仕組みです。
PHP のセッションは、ユーザーを識別する「セッション ID」と Web サーバーにデータを保存する「セッション変数($_SESSION)」により実現されます。
クッキーはデータをユーザーのブラウザに保存しますが、セッションはデータ(セッション変数)を Web サーバー上に保存し、セッション ID のみをクッキー(セッションクッキー)に保存します。
クッキーの問題点
クッキーを使った情報の保持には、以下のような問題点があります。
- データがクライアント側で保存される
- クッキーで保存されたデータは、クライアント側で自由に削除したり変更することが可能なため、クッキーの情報を元にアプリケーション全体の挙動を左右するような判定を行うのは問題があります。
- 実データがネットワーク上を流れる
- 通信系路上にリクエスト情報をロギングするような通信機器やソフトウェアがあると、クッキー情報が漏洩する可能性があります。
セッションは以下が、クッキーと異なります。
- データがサーバー側で保存される。
- ネットワーク上を流れるのは、セッション ID のみ。
session_start() 命令
セッションを利用するには、ページの先頭で、session_start() 関数を実行します。
session_start()関数は、必ず、Webブラウザへの出力が行われる前に、呼び出す必要があります。(setcookie()関数と同様、HTTPプロトコルの制約)
session_start() 関数を呼び出した後は、スーパーグローバル変数の1つであるセッション変数「$_SESSION」が使えるようになり、データを保存することができます。
セッション変数は、セッションを終了するまでページを移動しても利用することが可能です。$_SESSION に値を格納すると、同一のセッションで値を共有できます。(この変数は,同じ名前でもセッションごとに値が異なります。)
また、セッションは、session_start() が記述されているページでのみセッション変数「$_SESSION」を利用することができます。
デフォルトの設定でセッションを開始したときは,Web ブラウザを閉じるまで同じセッション(ID)が有効で Web ブラウザを閉じるとセッションは終了します。
標準ではセッション変数は、セッションファイルに保存され、保存場所は「php.ini」の「session.save_path」で指定されています。
以下が、session_start() 関数の構文です。
bool session_start ([ $options ] )
パラメータ
$options(array):オプションの連想配列を指定することができます。これは、現在設定されている セッションの設定ディレクティブを上書きします。 連想配列のキーにはプレフィックス session. を含めてはいけません。
戻り値
セッションが正常に開始した場合に TRUE、それ以外の場合に FALSE を返すします。
session_start() 関数は、初めてのアクセスか、2回目以降のアクセスかを判別します。
初めてのアクセスの場合は、「セッション ID」生成します。そうでない場合は、セッションクッキーまたは GET, POST により渡された「セッション ID」 に基づき現在のセッションを復元します。
以下は、セッションを使った簡単なカウンタの例です。
初回アクセス時には、セッション変数 $_SESSION['count'] がまだ存在しないので、その場合は $_SESSION['count'] に「1」をセットします。
セッション変数 $_SESSION['count'] がすでに存在する場合は、値を「1」増やします(++)。
最後に echo 文で、現在のアクセス数を表示します。その際にセッション変数をエスケープ処理して出力します。
<?php //セッション開始 session_start(); ?> <!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <title>セッションを使った簡単なカウンタ</title> </head> <body> <?php if(!isset($_SESSION['count'])){ $_SESSION['count'] = 1; //初回のアクセス }else{ $_SESSION['count'] ++; //2回目以降のアクセス } //変数を出力する際はエスケープ処理します echo "<p>アクセス回数: " . h($_SESSION['count']) . "</p>"; //エスケープ処理を行う関数 h() function h($str) { return htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); } ?> </body> </html>
セッション変数「$_SESSION」は、通常の変数と同じように配列データも代入することができます。以下は2つのページで、セッションが渡されているのを確認する例です。
<?php //セッション開始 session_start(); ?> <!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <title>セッションのサンプル1</title> </head> <body> <?php $_SESSION['test'] = array('php' => 'PHP: Hypertext Preprocessor', 'html' => 'Hyper Text Markup Language', 'ajax' => 'Asynchronous JavaScript And XML', 'css' => 'Cascading Style Sheet'); ?> <p><a href="session_sample2.php">session_sample2.php(確認ページへ)</a></p> </body> </html>
<?php //セッション開始 session_start(); ?> <!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <title>セッションのサンプル2</title> </head> <body> <?php if(isset($_SESSION['test'])) { foreach($_SESSION['test'] as $key => $value) { //値の出力はエスケープ処理します。 echo "$key : ". h($value). "<br>"; } }else{ echo "Session はありません。"; } //エスケープ処理を行う関数 h() function h($str) { return htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); } ?> <p><a href="session_sample1.php">session_sample1.php(戻る)</a></p> </body> </html>
最初に「session_sample1.php」にアクセスし、その後リンクから「session_sample2.php」にアクセスして、登録したセッション情報が表示されれば成功です。
セッションの流れ
以下では Firefox の Firebug で前述の session_counter.php を使ってセッション ID を確認してみます。
Firebug を起動して、「ネット」タブを選択します。
最初のアクセスでは、リクエストヘッダーにセッションクッキーがセットされていないので、Web サーバーは「セッション ID」生成します。
Web サーバーからのレスポンスヘッダーに「Set-Cookie」と言う項目があり、「PHPSESSID=l8clhs9sv7a682dos0637qu0g1; path=/」となっていて、これがセッションクッキーです。
「l8clhs9sv7a682dos0637qu0g1」の部分が「セッション ID」で、サーバーがランダムに生成する数字とアルファベットからなる文字列(32桁)です。
「PHPSESSID」は、セッション名(セッションIDを渡す際の変数名)でデフォルトではこのように PHPSESSID になっています。セッション名は、session_name() 関数でセットすることもできます。
このレスポンスヘッダーを受け取ったブラウザは、「PHPSESSID」という名前のセッションクッキーを保存します。
Web サーバはセッション ID を元に、$_SESSION 変数に格納された値を保持するためのファイルを Web サーバ上に生成します。(この時点でファイルの中身は空です。)
そして PHP スクリプトの実行の完了とともに、$_SESSION 変数に格納された値を Web サーバ上の生成されたセッションファイルに書き込みます。($_SESSION 変数に格納された値はネットワーク上を流れません)
続いて、「Cookie」タブを選択してみます。
セッションクッキーの名前(PHPSESSID)と内容(l8clhs9sv7a682dos0637qu0g1)が確認できます。
続いて、ブラウザのリロードボタンをクリックして、ページを再読み込みします。
2回目のアクセスでは、ブラウザからのリクエストヘッダーに「Cookie」という項目が表示され、ブラウザからセッション ID「PHPSESSID=l8clhs9sv7a682dos0637qu0g1」が Web サーバーに送信されているのがわかります。
セッション ID がクッキーによって送信されると、session_start() 関数は Web サーバー上にセッションファイルがあるかどうか確認し、ファイルが存在した場合は、ファイルの中身を取り出して、$_SESSION の値を復活させます。
続いて、「Cookie」タブを選択してみます。
セッションクッキーの名前(PHPSESSID)と内容(l8clhs9sv7a682dos0637qu0g1)が同じことが確認できます。
参考サイト:
10日で覚えるPHPのキソ 第 10 回 セッション(SESSION)
第8回 セッションの仕組みを知ろう (その1)
セッション名の取得・設定
セッション名を取得または設定するには、session_name() 関数を使用します。
以下が構文です。
string session_name([ $name ] )
パラメータ
- $name(string):セッションの名前。セッション名は英数字のみで構成されている必要があり、また数字だけで構成することはできず、少なくとも1文字以上の英字が必要です。
デフォルトのセッション名は「PHPSESSID」です。
この値はセッションクッキーのキーにも使われます。$_COOKIE[session_name()]
戻り値
その時点でのセッションの名前を返します。
session_name() は、パラメータを指定せず実行すると現在のセッション名を返します。
パラメータ name を渡すと、session_name() は現在のセッション名を上書きして元のセッション名を返します。
リクエストが開始される際にセッション名はリセットされ、 session.name(php.ini) に保存されたデフォルト値に戻ります。 よって、各リクエスト毎に session_start() を呼び出す前に session_name() を呼び出す必要があります。
以下はセッション名を変更して、元のセッション名と変更後のセッション名を表示する例です。
<?php //セッション名を MYSESSION に変更 $default_session_name = session_name('MYSESSION'); session_start(); ?> <!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <title>セッション名の変更</title> </head> <body> <?php echo "<p>デフォルトのセッション名: " . $default_session_name . "</p>"; echo "<p>変更後のセッション名: " . session_name() . "</p>"; ?> </body> </html>
全ての PHP スクリプトでセッション名を変更
全ての PHP スクリプトでセッション名を変更するには、php.ini を編集するか .htaccess を利用します。
//php.ini session.name = MYSESSION
//.htaccess php_value session.name MYSESSION
セッション ID の取得
セッション ID を取得するには、session_id() 関数を使用します。以下が構文です。
string session_id ([ $id ] )
パラメータ
- $id(string):id が指定された場合、現在のセッション ID を置換します。その際、この関数は session_start() より前にコールされている必要があります。
パラメータを指定しない場合は、現在のセッション ID を返します。
戻り値
session_id() は現在のセッションのセッション ID を返します。 現在のセッションが存在しない (現在のセッション ID が存在しない) 場合は空文字列 ("") を返します。
以下は、セッション ID を表示する例です。セキュリティの面からは,セッション ID を画面に表示させるのは好ましいことではありませんが,説明上表示しています。
<?php //セッション開始 session_start(); ?> <!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <title>セッション ID の取得</title> </head> <body> <?php echo "<p>現在のセッション ID: " . session_id() . "</p>"; //vstcc9s2sdd2o6c2jtq44u3pbncrkkhk のような値(ID)が表示されます ?> </body> </html>
※同じセッション ID を使い続けることは、セキュリティ上好ましくないので、session_regenerate_id() 関数を使ってセッション ID を変更するようにします。
セッション変数の破棄
セッション変数を破棄するには、変数の割当を解除する unset() 関数を使用します。以下が構文です。
void unset( $var [, $... ] )
パラメータ
- $var(mixed):破棄(削除)したい変数
戻り値
値を返しません。
全てのセッション変数を破棄(初期化)したい場合は、以下のように空要素の配列をセッション変数($_SESSION)に代入します。
$_SESSION = array();
【注意】unset($_SESSION) によって全ての $_SESSION を初期化してはいけません。 $_SESSION スーパーグローバル変数を用いたセッション変数の登録(使用)ができなくなってしまいます。
以下は、セッション変数を破棄する例です。
<?php //セッション開始 session_start(); ?> <!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <title>セッション変数の破棄</title> </head> <body> <?php $_SESSION['data1'] = "sample 1"; $_SESSION['data2'] = "sample 2"; $_SESSION['data3'] = "sample 3"; echo "<pre>"; print_r(h($_SESSION)); echo "</pre>"; unset($_SESSION['data1']); echo "<p>\$_SESSION['data1'] を破棄しました。</p>"; echo "<pre>"; print_r(h($_SESSION)); echo "</pre>"; $_SESSION = array(); echo "<p>\$_SESSION(全てのセッション変数)を破棄しました。</p>"; echo "<pre>"; print_r(h($_SESSION)); echo "</pre>"; //エスケープ処理を行う関数 h() function h($str){ if(is_array($str)){ //$str が配列の場合、h()関数をそれぞれの要素について呼び出す return array_map('h', $str); }else{ return htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); } } ?> </body> </html>
セッションの破棄
セッションに登録されたデータを全て破棄するには、session_destroy() 関数を使用します。
この関数は、 セッションに関するグローバル変数($_SESSION)を破棄しません。 また、セッションクッキーを破棄しません。 セッション変数の利用を再開するには session_start() を呼び出す必要があります。
ユーザーがログアウトするときのように、セッションを切断するには、 セッション ID の割り当ても解除する必要があります。セッション ID の受け渡しに クッキーが使用されている場合(デフォルト)には、セッションクッキーも 削除されなければなりません。
以下が構文です。
bool session_destroy ( void )
パラメータ
ありません。
戻り値
成功した場合に TRUE を、失敗した場合に FALSE を返します。
以下は、セッションを破棄する例です。session_destroy() 関数の実行と合わせて、セッション変数とセッションクッキーも削除するようにします。
<?php //セッション開始(初期化) session_start(); $_SESSION['sample1'] = "PHP"; $_SESSION['sample2'] = "JavaScript"; //後で確認のために表示するので、セッション変数を別の変数に保存 $old_session = $_SESSION; //セッション変数を全て初期化(解除) $_SESSION = array(); //セッションクッキーの削除 if(isset($_COOKIE[session_name()])){ //setcookie() は、他のあらゆる出力よりも前に記述する必要があります setcookie(session_name(), '', time()-42000, '/'); } //最終的に、セッションを破壊する session_destroy(); //エスケープ処理を行う関数 h() function h($str){ if(is_array($str)){ //$str が配列の場合、h()関数をそれぞれの要素について呼び出す return array_map('h', $str); }else{ return htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); } } ?> <!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <title>セッションの破棄</title> </head> <body> <?php echo "<p>破棄前のセッション情報(\$_SESSION)</p>"; echo "<pre>"; print_r(h($old_session)); echo "</pre>"; echo "<p>破棄後のセッション情報(\$_SESSION)</p>"; echo "<pre>"; print_r(h($_SESSION)); echo "</pre>"; ?> </body> </html>
セッションの有効期限
セッションの有効期限は php.ini の session.gc_maxlifetime で定義され、既定値は 1440 秒 (24分) です。
セッションの有効期限 を不用意に延ばすことは、セキュリティ面のリスクを高めることになるので注意が必要です。
セッションの有効期限に関する設定項目は以下になります。
有効期限の切れたセッションファイルは、PHP のガーベッジコレクション(GC)が行います。
ガーベッジコレクション(GC)は、session_start()が呼ばれた時(誰かがアクセスしてきたタイミング)に一定の確率で起動するようになっています。
この確立を設定するのが、session.gc_probability(デフォルト:1)と
session.gc_divisor(デフォルト:100)で、
session.gc_probability / session.gc_divisor(デフォルト:1/100)が
ガーベッジコレクション(GC)の起動する確立になります。
セッション ID の変更
セッション ID が漏洩すると、他人にセッションを乗っ取られる(セッションハイジャック)可能性があります。セッション ID の定期的な変更はセッションハイジャックに対する有効な対策です。
セッション ID は、session_regenerate_id() 関数を呼び出すことで変更することができます。
session_start() 関数でセッションを開始した後に session_regenerate_id() 関数を呼び出します。session_regenerate_id() 関数は、現在使っているセッションを終了させることなくセッション ID だけを新しい値に変換してくれます。
以下が、session_regenerate_id() 関数の構文です。
bool session_regenerate_id([ $delete_old_session ] )
パラメータ
$delete_old_session(bool):関連付けられた古いセッションを削除するかどうか。デフォルトは FALSE ですが、必ず TRUE を指定するようにします。
戻り値
成功した場合に TRUE を、失敗した場合に FALSE を返します。
注意
session_regenerate_id() を 呼ぶことでセッションを消失する可能性があります。以下は PHP マニュアルからの引用です。
警告 現在の session_regenerate_id は、不安定なネットワークをうまく扱えません。 たとえば、モバイルネットワークや WiFi ネットワークです。 よって、 session_regenerate_id を 呼ぶことで、セッションの消失を経験するかもしれません。
また、session_regenerate_id() の呼び出しでは問題がなく、session_regenerate_id(TRUE) のようにパラメータに TRUE または true を指定するとセッションが消えるという現象も発生(確認)しました。
セッションが消えてしまう場合、session_regenerate_id() を疑う必要があるかも知れません。
以下は、session_regenerate_id() 関数を使ってセッション ID を変更する例です。
セキュリティの面からは,セッション ID を画面に表示させるのは好ましいことではありませんが,確認のためセッション ID の最初の 10文字を表示しています。
セッション ID は session_id() 関数で取得することができます。
ブラウザのリロードボタンをクリックして、ページを再読み込みすると、セッション ID が変更されていることが確認できます。またカウンタは1つずつ増えていきます。
20行目の isset($_COOKIE[session_name()]) は、セッションクッキーが設定されているかどうかを判定しています。session_name() 関数は、現在のセッション名を取得または設定する関数です。
セッションがクッキーを使ってセッション ID を保存している場合、セッション ID はセッションの名前を持つクッキーに保存されています。
<?php session_start(); //セッション開始 $old_id = session_id(); //現在のセッションIDを取得 $disp_old_id = substr($old_id, 0, 10); //先頭の10文字を取得 session_regenerate_id(TRUE); //セッションIDを変更 // または session_regenerate_id(); $new_id = session_id(); //変更後のセッションIDを取得 $disp_new_id = substr($new_id, 0, 10); ?> <!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <title>セッション ID を変更する</title> </head> <body> <?php if (!isset($_COOKIE[session_name()])){ echo "初回のアクセスです。"; }else{ echo "前回のセッションID の最初の10文字: ".$disp_old_id."<br>"; echo "今回のセッションID の最初の10文字: ".$disp_new_id."<br>"; } if(!isset($_SESSION['count'])){ $_SESSION['count'] = 1; //初回のアクセス }else{ $_SESSION['count'] ++; //2回目以降のアクセス } echo "<p>アクセス回数: " . $_SESSION['count'] . "</p>"; ?> </body> </html>
セッションの設定オプション
php.ini の設定によりセッションの動作が変わります。PHP マニュアルにいろいろなオプションが記載されています。
- session.hash_function(デフォルト:0 / 推奨:1)
- セッション ID を生成するハッシュ関数を指定します。デフォルトの 0 は MD5(128 bits)です。1 は SHA-1(160 bits)で、こちらの方がビット数が多いので、その分だけ安全と言えます。
- session.save_path
- セッションファイルを保存するディレクトリを指定します。共有サーバーでは、他人がアクセスできないディレクトリを指定するようにします。(XAMPP のデフォルトは、session.save_path="C:\xampp\tmp" )
- 現在のセッションデータの保存先は、session_save_path() 関数を引数なしで実行しても確認できます。
- session.use_cookies(デフォルト:1 / 推奨:1)
- Cookie によりセッション管理を行うかどうかを指定します。必ず 1 (有効)に指定します。
- session.use_only_cookies(デフォルト:1 / 推奨:1)
- session.use_only_cookies は、 このモジュールがクライアント側へのセッション ID の保存に Cookie のみ を使用することを指定します。 この設定を有効にすることにより、セッション ID を URL に埋め込む攻撃を防ぐことができます。URL によるセッション管理には様々な問題があるため、必ず 1 に指定します。( PHP 5.3.0 以降で、デフォルトは 1)
- session.use_trans_sid(デフォルト:0 / 推奨:0)
- 透過的なセッション ID の付加をするかどうかを指定します。この機能はセキュリティ上の問題が大きいため、特別な場合を除き使用しない(0 を指定)ようにします。この設定を有効にしてしまうと、Cookie からセッション ID を取得できなかった場合、自動的に相対パスのあらゆる URL にセッション ID が付加されることになり、セッション ID が漏洩する危険性が高まります。
セッションのセキュリティ対策
URL によるセッション管理の禁止、セッション ID の定期的な変更、セッション固定化攻撃や CSRF 攻撃への対策等を講じる必要があります。
セッションハイジャック
他人のセッションIDがわかれば、そのセッションを乗っ取ることができ、そのような攻撃をセッションハイジャックと言い、非常に危険な攻撃です。
PHP のセッションでは、セッション名(デフォルトは PHPSESSID)とセッション ID(デフォルトはランダムな MD5 値)がわかれば、セッションハイジャックが可能です。
攻撃者がセッション ID を知るには、いろいろな方法があるので以下のような対策を行う必要があります。
送信元(Referer)情報によるセッション ID の漏洩
PHP のセッション ID はクッキーに保存されるか、URL に埋め込まれますが URL に埋め込まれた場合、セッション ID が漏洩する危険性が高まります。そのため以下のように php.ini の設定を指定することで、URL によるセッション管理を禁止して、クッキーのみでセッション管理を行うようにすることができます。(デフォルトの設定)
//php.ini session.use_cookies=1
また、php.ini の session.use_trans_sid(デフォルトは無効)を有効にしてしまうと、クッキーからセッション ID を取得できなかった場合、自動的に相対パスの URL にセッション ID が付加されてしまうので危険性が高まります。session.use_trans_sid は有効にしないようにします。
//php.ini session.use_trans_sid=0
クロスサイトスクリプティング(XSS)によりクッキー情報の漏洩
クロスサイトスクリプティング(XSS)が可能なページがあれば、XSS によりクッキー情報が盗まれてセッションハイジャックされる可能性があります。XSS 対策を確実に行うようにします。
セッション ID を保存するセッションクッキーに httponly 属性を指定する方法も有効です。(但し、JavaScript/jQuery でクッキーの操作ができなくなります)。
通信の盗聴によりセッション ID の漏洩
可能であれば、SSL/TSL 通信を利用してセッションクッキーに secure 属性を指定します。
セッション ID の推測
PHP のセッション ID は、デフォルトでは MD5(128ビット)によるハッシュ値なので、簡単には推測することは難しいです。但し、自分でセッション ID を生成する場合は、推測が難しい値を生成する必要があり注意が必要です。
php.ini の設定で、以下のようにすることでセッション ID の値を SHA1(160ビット)のハッシュ値に変更できるので、こちらの方がより安全になります。
//php.ini session.hash_function=1
PHP 5.3 以降では、サーバーでサポートされていれば、session.hash_function = sha256 や session.hash_function = sha512 の指定もできるようです。
また、セッション名はセッションクッキーのキーとして保存されるので、デフォルトの「PHPSESSID」のままだとセッションクッキーだと特定されてしまいます。php.ini で以下のようにセッション名を変更するのも対策になると思います。
セッション名は英数字のみで構成されている必要があり、少なくとも英字が1文字含まれている必要があります。
//php.ini session.name=X07MYS3F
セッション固定化攻撃
セッション ID を推測する代わりに、攻撃者がセッション ID を指定する攻撃を「セッション固定化攻撃」と言います。対策は、URL によるセッションの管理を禁止することです。 php.ini で以下のように設定します。
//php.ini session.use_only_cookies=1
セッション ID の変更
セッションが開始されるページや、ログイン直後、また一定時間が経過するたびに、session_regenerate_id()関数を使ってセッションIDを変更することは、セッションハイジャック全般に対する有効な対策になります。
パラメータは、古いセッションファイルを削除するかどうかの真偽値で、必ず TRUE を指定するようにします。
session_start(); //セッション開始 session_regenerate_id(TRUE); //セッションIDを変更 // TRUE を指定するとセッションが消失する場合は、以下のように TRUE を指定しない // session_regenerate_id();
セッションファイルによるセッション ID の漏洩
セッションファイル名は「sess_l8clhs9sv7a682dos0637qu0g1」のように「sess_」+「セッションID」により構成されています。つまりセッションファイル名がわかれば、セッション ID もわかってしまいます。
セッションファイルを保存するディレクトリは、php.ini で以下のように設定できるので、共有サーバーでは、他人がアクセスできないディレクトリを指定します。
session.save_path="ディレクトリを指定"
CSRF攻撃
何らかの方法でユーザーにリクエストを送信させることにより、正当なユーザーに意図しない処理を実行させる攻撃方法をCSRF攻撃(クロスサイトスクリプティングフォージェリー)と言います。
これでは何のことかよくわからないので、以下は「情報処理推進機構(IPA)リクエスト強要(CSRF)対策」からの引用です。
リクエスト強要(CSRF)
リクエスト強要(CSRF:Cross-site Request Forgery)とは、別のサイトに用意したコンテンツ上の罠のリンクを踏ませること等をきっかけとして、インターネットショッピングの最終決済や退会等Webアプリケーションの重要な処理を呼び出すようユーザを誘導する攻撃である。
ブラウザが正規の Webコンテンツにアクセスした際には毎回、セッションを維持するために所定の Cookie、Basic認証データあるいは Digest認証データがブラウザから Webサーバ宛に送出されるという性質を、この攻撃は悪用する。
ユーザの意図に反することを検証できないWebアプリケーション
この攻撃の対象となるのは、トランザクション投入のきっかけとなったフォーム画面が自サーバから供給(POST)されたものであることを確認していない Webアプリケーションである。言い換えれば、ブラウザから自動的に得られる情報のみに基づいてユーザやセッションを識別しているナイーブな Webアプリケーションである。このような Webアプリケーションは、ユーザの意図に反してリクエストが送出されたことを検証できない。
具体的には次のようなWebアプリケーションが対象となる。
- Cookieを用いてセッションIDを搬送している Webアプリケーション
- Basic認証を用いているWebアプリケーション
- Digest認証を用いているWebアプリケーション
- そのほかWebクライアントから自動で得られる情報にもとづきユーザやセッションを識別しているWebアプリケーション
リクエスト強要(CSRF)のメカニズム
攻撃対象とは別の Webサーバに罠のリンクが用意される。ユーザがその罠を踏んで、攻撃手段が仕込まれる。
Cookieをはじめとする認証データ、識別データは、HTTPリクエストの対象コンテンツ(「目的地」)が正規のものでありさえすれば、Webアプリケーションが用意したものでない偽のフォームやハイパーリンク(「偽の出発地」)から発せられたものであっても送出される。例えば、Cookieを用いてセッション管理を行っている Webアプリケーションの場合、ブラウザから自動送出される Cookieに WebセッションIDを搭載し、サーバがそのIDを見て個々のブラウザを識別している。
特段の対策がとられていない Webアプリケーションは、HTTPリクエストに伴って送られてきたCookieに正規のセッションIDが入っていさえすれば、そのリクエストが受け入れて、ユーザ本人の意志に反した処理を行ってしまうおそれがある。
特に、ユーザがログイン中に攻撃されると、ログイン後にしか操作しないような重要なトランザクションを送ってしまうことになる。(注: この攻撃の構図は、ユーザがログインしていることを要さない画面のフォームについても成立しうる。)
リクエスト強要(CSRF)対策
リクエスト強要(CSRF)への対策は、ユーザ本人以外の者が捏造(ねつぞう)したコンテンツに基づいて発せられたHTTPリクエストを Webアプリケーションが受け付けないようにすることである。
そのためには、代金決済やコミュニティ脱退等の重要な処理の場面において、秘密の「照合情報」をWebアプリケーションとブラウザの間でやりとりし、第三者が用意した偽のコンテンツから発せられたリクエストを区別できるようにする。
具体的には、フォームを表示するプログラムによって他者が推定困難なランダム値を hiddenフィールドとして埋め込み送信し、フォームデータを処理するプログラムによってそのランダム値がフォームデータ内に含まれていることを確認する。
そのランダム値がフォ-ムデータに含まれていない場合、処理を見合わせるようにする。
対策
対策の1つが「トークン方式」で、セッションの開始時やログイン時にトークン(ランダムな文字列)を発行し、そのトークンが一致する場合にのみ処理を実行し、一致しない場合は不正なリクエストとして扱います。
固定トークンの発行
処理ページの直前のページ(入力ページ等)に以下のようなコードを記述してトークンを発行します。固定トークン方式では、セッション中にトークンの値が変わりません。
【更新】※当初は sha1() と uniqid()、mt_rand() を使ってトークンを生成する記述になっていましたが、セキュリティ的に問題があるようなので以下に書き換えました。
//CSRF対策の固定トークンを生成 if(!isset($_SESSION['ticket'])){ //セッション変数にトークンを代入(PHP7.0以降) $_SESSION['ticket'] = bin2hex(random_bytes(32)); //PHP7.0未満(PHP5.3以降)の場合 //$_SESSION['ticket'] = bin2hex(openssl_random_pseudo_bytes(32)); } //変数 $ticket にトークンの値を代入(隠しフィールドに挿入する値) $ticket = $_SESSION['ticket'];
まずセッション変数 $_SESSION['ticket'] に値がセットされているかを isset() 関数で調べます。セットされてない場合は、セッション変数にトークンを代入します。
random_bytes() は暗号論的に安全な、疑似ランダムなバイト列(バイナリデータ)を生成する関数で、引数には返すべきランダムな文字列の長さをバイト単位で指定します。
random_bytes() で返されるバイナリデータはそのままでは使えないので bin2hex() を使って16進文字列表現(ASCII文字列)に変換します。
//疑似ランダムなバイナリデータを生成 $rb = random_bytes(32); var_dump($rb); //string(32) "o�%�b���������Pi��y(��H�*~c�|" //16進文字列表現に変換 echo bin2hex($rb); //6fc825db1462a4f317d2ffc18cadd5f45069909b79281482a848892a7e63b47c
random_bytes() は PHP7.0 以降でしか使えないので、PHP7.0 未満(PHP5.3以降)の場合は、openssl_random_pseudo_bytes() を使います。
//疑似ランダムなバイナリデータを生成 $rb = openssl_random_pseudo_bytes(32); var_dump($rb); //string(32) "g��po��r?� Ԉ��?�拌J7������" //16進文字列表現に変換 echo bin2hex($rb); // 67f3d6706f8ef4723fdf170ad488d3e43f07d3e68b8c4a37f1801af59afadcd4
トークンの POST
同時にトークンの値($ticket)をフォームの隠しフィールド(input type="hidden")を設定します。
<form action="session_csrf2.php" method="post"> ・・・ <!--以下はトークンを POST する隠しフィールド「ticket」--> <input type="hidden" name="ticket" value="<?php echo $ticket; ?>"><br> <input type="submit" value="確認画面へ" /> </form>
処理ページでのトークンの確認
処理ページでは、POST された隠しフィールドの値が、サーバーに保存されている値と一致するかを調べて、一致した場合のみ処理を実行するようにします。
//固定トークンを確認(CSRF対策) if(isset($_POST['ticket'], $_SESSION['ticket'])){ $ticket = $_POST['ticket']; if($ticket !== $_SESSION['ticket']){ die('不正なアクセスです。(トークン不一致)'); } }else{ die('不正なアクセスです。(トークンなし)'); }
$_POST['ticket'] は、フォームから POST された隠しフィールドの値で、$_SESSION['ticket'] はセッション変数で、直前のページ(入力ページ等)から処理ページに POST された場合、これらの変数はセットされているはずです。
そこでまず、isset() 関数でこれらの値がセットされているかを調べ、これらの変数がセットされていない場合は、不正なアクセスと判断して die() で即座に処理を中止します。
これらの変数がセットされていた場合は、それらの値が一致するかを確認し、一致しない場合は不正なアクセスと判断できるので die() で即座に処理を中止します。
正規のページを経由せずに、外部の攻撃ページから POST された場合はトークンがセッションに保存されていません。またトークンの値は外部からはわからないので、これらをチェックすることで CSRF 対策になります。
このような実装を行えば、トークンの値がわからない限り安全です。
または、処理ページの直前のページでパスワード入力を要求して、処理ページではパスワードが正しい場合のみ処理をするという方法もあります。
但し、クロスサイトスクリプティング(XSS)脆弱性があればトークン方式を実装しても、隠しフィールドのトークンの値が盗まれる可能性があり、その場合はトークン方式による CSRF 対策は無効化されてしまう可能性があります。
サンプル
以下は、メールアドレスを登録するサンプルです。「メールアドレスを入力するページ」「メールアドレスを確認して登録するページ」「完了ページ」からなりますが、このサンプルは CSRF 対策が目的なので、登録などの処理は省略してあります。以下が使用するファイルです。
- session_csrf1.php(入力ページ)
- session_csrf2.php(確認ページ)
- session_csrf3.php(完了ページ)
- session_validation.php(検証やエスケープ処理の関数を記述したファイル)
以下は入力ページです。
16行目:初回アクセス時は、$_SESSION['email'] はセットされていないので「null」で初期化します。これを行わないと、37行目の $email の出力で「Undefined index」という Notice エラーが発生します。
19~22行目:CSRF対策の固定トークンを発行します。
25行目:変数 $ticket にトークンの値を代入しています。この値をフォームの隠しフィールド(39行目)に挿入します。
session_csrf1.php
<?php //セッション開始 session_start(); //セッションIDを変更(セッションハイジャック対策) session_regenerate_id(TRUE); //session_regenerate_id(); //セッションが消失する場合は、TRUE を削除(または session_regenerate_id を削除) //検証及びエスケープ処理の関数のファイルの読み込み require 'session_validation.php'; //POSTされたデータがあれば独自に定義した関数でチェック if(count($_POST) > 0) { $_POST = checkInput($_POST); } //セッション変数に値が代入されていればその値を。そうでなければNULLで初期化 $email = isset($_SESSION['email']) ? $_SESSION['email'] : NULL; //CSRF対策の固定トークンを生成 if(!isset($_SESSION['ticket'])){ //セッション変数にトークンを代入 $_SESSION['ticket'] = bin2hex(random_bytes(32)); } //変数 $ticket にトークンの値を代入 $ticket = $_SESSION['ticket']; ?> <!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <title>CSRF 対策(固定トークン方式)入力画面</title> </head> <body> <p>E-mail アドレスの登録</p> <form action="session_csrf2.php" method="post"> <label for="email">E-mail</label> <input type="text" name="email" id="email" value="<?php echo h($email); ?>" size="40"> <!--以下は確認ページへトークンをPOSTする隠しフィールド「ticket」--> <input type="hidden" name="ticket" value="<?php echo $ticket; ?>"><br> <input type="submit" value="確認・登録画面へ" /> </form> </body> </html>
以下は、確認ページです。
14~21行目:固定トークンを確認しています。
29行目:エラーメッセージを格納する配列を初期化しています。この例ではメールアドレスの登録だけなので、配列にする必要はありませんが、複数の項目を検証する場合を考えて配列にしています。
31~38行目:POSTされたデータの内容を検証しています。そしてエラーがなければ42行目で POSTされたデータをセッション変数 $_SESSION['email'] に保存します。
52行目から:エラーがある場合は、エラーのメッセージを表示し、入力ページへ戻るボタンを表示します。フォームの隠しフィールドにはトークンの値を入れておきます。
エラーがない場合は、確認のためのメールアドレスを表示し、登録画面へのボタンと、入力画面に戻るボタンを表示し、隠しフィールドにはトークンの値を入れておきます。
session_csrf2.php
<?php //セッション開始 session_start(); //セッションIDを変更(セッションハイジャック対策) session_regenerate_id(TRUE); // または session_regenerate_id(); //検証及びエスケープ処理の関数のファイルの読み込み require 'session_validation.php'; //POSTされたデータを独自に定義した関数でチェック $_POST = checkInput($_POST); //固定トークンを確認(CSRF対策) if(isset($_POST['ticket'], $_SESSION['ticket'])){ $ticket = $_POST['ticket']; if($ticket !== $_SESSION['ticket']){ die('不正な入力です。(トークン不一致)'); } }else{ die('不正な入力です。(トークンなし)'); } //POSTされたデータを変数に格納 $email = isset($_POST['email']) ? $_POST['email'] : NULL; //POSTされたデータを整形(前後にあるホワイトスペースを削除) $email = trim($email); //エラーメッセージを保存する配列の初期化 $error = array(); if($email == ''){ //メールアドレスが空でないかをチェック $error[] = '*メールアドレスは必須です。'; }else{ //メールアドレスを正規表現でチェック $pattern = '/^([a-z0-9\+_\-]+)(\.[a-z0-9\+_\-]+)*@([a-z0-9\-]+\.)+[a-z]{2,6}$/uiD'; if(!preg_match($pattern, $email)){ $error[] = '*メールアドレスの形式が正しくありません。'; } } if(count($error) === 0) { //エラーがなければPOSTされたデータをセッション変数に保存 $_SESSION['email'] = $email; } ?> <!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <title>CSRF 対策(固定トークン方式)確認画面</title> </head> <body> <?php if(count($error) > 0){ //エラーがあった場合 //エラーを表示 foreach($error as $val) { echo '<p style="color:red">'.h($val) .'</p>'; } ?> <form action="session_csrf1.php" method="post"> <!--入力ページへトークンをPOSTする、隠しフィールド「ticket」--> <input type="hidden" name="ticket" value="<?php echo $ticket; ?>"> <p><input type="submit" value="入力画面へ戻る" ></p> </form> <?php }else{ //エラーがない場合 echo '<p>以下の E-mail アドレスを登録します。</p>'; echo '<p>' . h($email). '</p>'; ?> <form action="session_csrf3.php" method="post"> <!--完了ページへトークンをPOSTする、隠しフィールド「ticket」--> <input type="hidden" name="ticket" value="<?php echo $ticket; ?>"> <p><input type="submit" value="登録する" ></p> </form> <form action="session_csrf1.php" method="post"> <!--入力ページへトークンをPOSTする、隠しフィールド「ticket」--> <input type="hidden" name="ticket" value="<?php echo $ticket; ?>"> <p><input type="submit" value="入力画面へ戻る" ></p> </form> <?php } ?> </body> </html>
以下は、完了ページです。このページでは、本来ならメールアドレスを登録する処理やメールを送信する処理を行い、その結果により表示する内容を変更しますが、このサンプルではそれらの処理は省略しています。
15~22行目:トークンを確認しています。
31~39行目:メールアドレスを登録する処理が成功した場合、セッションを破棄します。
41行目:メールアドレスを登録する処理が失敗した場合の処理を記述します(省略)。
session_csrf3.php
<?php header("Content-type: text/html; charset=utf-8"); //セッション開始 session_start(); //セッションIDを変更(セッションハイジャック対策) session_regenerate_id(TRUE); // または session_regenerate_id(); //検証及びエスケープ処理の関数のファイルの読み込み require 'session_validation.php'; //POSTされたデータを独自に定義した関数でチェック $_POST = checkInput($_POST); //固定トークンを確認(CSRF対策) if(isset($_POST['ticket'], $_SESSION['ticket'])){ $ticket = $_POST['ticket']; if($ticket !== $_SESSION['ticket']){ die('不正な入力です。(トークン不一致)'); } }else{ die('不正な入力です。(トークンなし)'); } //変数にセッション変数の値を代入 $email = $_SESSION['email']; //登録やメール送信の処理などを記述(省略) //登録やメール送信の処理が成功した場合 //登録が成功した場合はセッションを破棄 $_SESSION = array(); //空の配列を代入し、すべてのセッション変数を消去 //セッションクッキーの削除 if(isset($_COOKIE[session_name()])){ //setcookie() は、他のあらゆる出力よりも前に記述る必要があります setcookie(session_name(), '', time()-42000, '/'); } //セッションを破棄 session_destroy(); //登録やメール送信の処理が失敗した場合(省略) ?> <!doctype html> <html lang="ja"> <head> <meta charset="utf-8"> <title>CSRF 対策(固定トークン方式)完了画面</title> </head> <body> <?php //登録やメール送信の処理が成功した場合 echo '<p>ありがとうございます。以下のアドレスの登録が完了いたしました。</p>'; echo '<p>'. h($email). '</p>'; //登録やメール送信の処理が失敗した場合(省略) ?> <p><a href="session_csrf1.php">入力画面へ戻る</a></p> </body> </html>
session_validation.php
<?php //入力値に不正なデータがないかなどをチェックする関数 function checkInput($var){ if(is_array($var)){ return array_map('checkInput', $var); }else{ //PHP 7.4.x で get_magic_quotes_gpc() は非推奨になりました //php.iniでmagic_quotes_gpcが「on」の場合の対策 /*if(get_magic_quotes_gpc()){ $var = stripslashes($var); }*/ if(preg_match('/\0/', $var)){ //NULLバイト攻撃対策 die('不正な入力です。'); } if(!mb_check_encoding($var, 'UTF-8')){ //文字エンコードのチェック die('不正な入力です。'); } //改行、タブ以外の制御文字のチェック if(preg_match('/\A[\r\n\t[:^cntrl:]]*\z/u', $var) == 0){ die('不正な入力です。制御文字は使用できません。'); } return $var; } } //エスケープ処理を行う関数 function h($str){ if(is_array($str)){ //$strが配列の場合、h()関数をそれぞれの要素について呼び出す return array_map('h', $str); }else{ return htmlspecialchars($str, ENT_QUOTES, 'UTF-8'); } }
関連ページ