PHP セキュリティに関するチートシート

OWASP 作成
ジャンプ先: 移動検索

はじめに

このページは、PHP のセキュリティに関する基本的なヒントを開発者や管理者に提供することを目的としています。Web アプリケーションのセキュリティを確保するためには、このページで説明するヒントだけでは十分でない可能性があることに留意してください。

PHP の概要

PHP は最も一般的に使用されているサーバーサイドプログラミング言語で、W3 Techs によると 81.8% の Web サーバーに採用されています。

オープンソースのテクノロジである PHP は、言語であると同時に Web フレームワークでもあるという点が独特で、標準的な Web フレームワーク機能が言語に組み込まれています。他の Web 言語と同様に、ライブラリなどの大規模なコミュニティが存在し、PHP でのプログラミングのセキュリティに寄与しています。PHP サイトのセキュリティ確保に取り組むにあたっては、この 3 つの側面 (言語、フレームワーク、ライブラリ) をすべて考慮する必要があります。

PHP は、慎重に設計された言語というよりは、むしろ成長を重ねてきた言語であるため、安全でない PHP アプリケーションを安易に記述してしまいがちです。PHP を安全に使用するためには、その潜在的な危険性をすべて把握しておく必要があります。

言語の問題点

弱い型指定

PHP は型指定に関してルーズです。つまり、PHP では不適切な型のデータがしかるべき型に自動変換されます。この機能は往々にして、開発者による誤りや不適切なデータの挿入を覆い隠してしまい、これが脆弱性につながります (例については下記の「入力処理」を参照)。

暗黙の型変換を行わない関数および演算子を使うようにします。たとえば、== ではなく === を使用します。すべての演算子に型指定の厳格なバージョンがあるわけではありません (より大きい、より小さいなど)。また、多くの組み込み関数 (in_array など) では、型指定の弱い比較関数が既定で使用されます。このことが適切なコードの記述を困難にしています。

例外処理とエラー処理

ほぼすべての PHP 組み込み関数や多くの PHP ライブラリは例外を使用せず、代わりに別の方法 (通知など) でエラーを報告します。このため、欠陥のあるコードでも実行を続けることが可能です。この結果として、多くのバグが覆い隠されてしまいます。PHP と競合する他の多くの言語とほとんどの高級言語では、開発者のミスや、開発者が予測できなかった実行時エラーが原因でエラー状態になると、プログラムは実行を停止します。これが最も安全な対応です。

次のコードを見てみましょう。このコードは、ユーザー名がブラックリストに含まれているかどうかを確認するデータベースクエリを使用して、特定の機能へのアクセスを制限します。

   $db_link = mysqli_connect('localhost', 'dbuser', 'dbpassword', 'dbname');
   function can_access_feature($current_user) {
       global $db_link;
       $username = mysqli_real_escape_string($db_link, $current_user->username);
       $res = mysqli_query($db_link, "SELECT COUNT(id) FROM blacklisted_users WHERE username = '$username';");
       $row = mysqli_fetch_array($res);
       if ((int)$row[0] > 0) {
           return false;
       } else {
           return true;
       }
   }
   if (!can_access_feature($current_user)) {
       exit();
   }
   
   // 機能のコードをここに記述します。


このコードでは、さまざまな実行時エラーが発生する可能性があります。たとえば、パスワードの誤りやサーバーの停止などによってデータベース接続が失敗したり、クライアント側で開かれた接続が、その後サーバーによって閉じられたりする場合があります。このような場合、mysqli_ 関数群は警告か通知を発しますが、例外や致命的エラーはスローしません。つまり、コードはそのまま続行するのです。変数 $rowNULL になります。PHP は $row[0]NULL と評価し、型指定が弱いため (int)$row[0]0 と評価します。最終的に can_access_feature 関数は true を返し、ブラックリストに掲載されているかどうかにかかわらず、すべてのユーザーにアクセスを許します。

これらのネイティブなデータベース API を使用する場合は、各所にエラーチェックを追加する必要があります。しかし、これには余計な作業が要求されるため省略されがちで、既定では安全でありません。また、多くの決まり切ったコード記述も必要とされます。 このため、ネイティブドライバーと入念なエラーチェックを採用せざるを得ない明確な理由がある場合を除き、データベースのアクセスには常に PHP データオブジェクト (PDO) を使用し、ERRMODE_WARNING フラグまたは ERRMODE_EXCEPTION フラグを指定することをお勧めします。

また、多くの場合、error_reporting 関数を使用してエラー報告レベルをできるだけ高く設定するのが最善です。エラーメッセージを抑制することは絶対にしないでください。必ず警告に従って、より強固なコードを記述してください。

php.ini

PHP コードの挙動は、たいていの場合、多くの構成設定値に依存します。これには、エラーの処理方法のような基本的な動作の変更も含まれます。このため、すべての環境で正しく動作するコードを記述するのは至難の業です。これらの設定に関する想定や要件がライブラリによって異なるため、サードパーティコードを適切に使用することが難しくなっています。一部について下記の「設定」で説明しています。

役に立たない組み込み関数

PHP にはセキュリティ用と思われる組み込み関数が多数用意されていますが (addslashesmysql_escape_stringmysql_real_escape_string など)、その多くはバグだらけで、実際にはセキュリティ問題の対処に役立ちません。これらの関数の一部は廃止され削除されつつありますが、下位互換性のポリシーから、これには長い時間がかかります。

PHP には array というデータ構造も用意されており、PHP コードや内部で広く使用されていますが、これは配列とディクショナリを一緒にした紛らわしいものです。この紛らわしさが原因で、経験を積んだ PHP 開発者でさえも Drupal SA-CORE-2014-005 (パッチを参照) のような重大なセキュリティの脆弱性を持ち込んでしまいます。

フレームワークの問題点

URL ルーティング

PHP に組み込まれている URL ルーティングメカニズムは、ディレクトリ構造内の ".php" で終わるファイルを使用したものです。ここから次のような脆弱性が生じます。

  • ファイル名をサニタイズしないすべてのファイルアップロード機能のリモート実行に関する脆弱性(すなわち、サービスを提供する側の Web サーバーが任意のコマンドを実行させられてしまいます)。アップロードされたファイルの保存時にその内容とファイル名を必ず適切にサニタイズしてください。
  • ソースコード (構成ファイルを含む) が、パブリックにアクセス可能なディレクトリにダウンロード用のファイル (静的な資産など) と一緒に保存されます。設定を誤るか、設定し忘れれば、機密情報が含まれているソースコードや構成ファイルが攻撃者によってダウンロードされるおそれがあります(言い換えれば、プライベートまたは実行専用であったはずのリソースを Web サーバーが提供します)。.htaccess を使用してアクセスを制限できます。これは既定では安全でないため理想的とはいえませんが、これに代わる手段が他にありません。
  • URL ルーティングメカニズムはモジュールシステムと同じです。これは、多くの場合、攻撃者がファイルを設計意図とは異なる侵入ポイントとして利用できることを意味します。ここから、認証メカニズムが完全に迂回されるという脆弱性が生じる可能性があります。つまり、コードを別ファイルに取り出す単純なリファクタリングによる脆弱性が発生するおそれがあります。これは PHP では特に簡単にできます。なぜなら、PHP にはグローバルにアクセス可能なリクエストデータ ($_GET など) があるので、リクエスト処理コードが関数定義内になくても、ファイルレベルのコードが、リクエストに基づいて動作する命令コードになり得るからです。
  • 適切な URL ルーティングメカニズムが存在しないため、開発者が独自の場当たり的な方式を開発することが多くなります。これらの方式は往々にして安全ではなく、異なるリクエスト処理機能に対して適切に認可制限を適用していません。

入力処理

PHP では、HTTP 入力を単純な文字列として処理するのではなく、HTTP 入力からクライアントが制御できる配列が作成されます。このことがデータについて混乱を招くおそれがあり、セキュリティ上のバグにつながりやすくなる可能性があります。たとえば、パスワードのリセット用コードなどで使用される "1 回限りの nonce" のメカニズムを単純化した次のコードを見てみましょう。

   $supplied_nonce = $_GET['nonce'];
   $correct_nonce = get_correct_value_somehow();
   
   if (strcmp($supplied_nonce, $correct_nonce) == 0) {
       // パスワードをリセットして OK
   } else {
       echo 'Sorry, incorrect link';
   }

攻撃者が次のようなクエリ文字列を使用した場合、

   http://example.com/?nonce[]=a

$supplied_nonce が配列になります。その後、strcmp() 関数が (この方がはるかに便利なのですが、例外をスローするのではなく) NULL を返します。型指定の弱さと、=== (同一性) 演算子ではなく == (相当性) 演算子を使用していることから、比較が成功します (PHP では式 NULL == 0 が真であるため)。したがって、攻撃者は正しい nonce を指定することなく、パスワードをリセットできます。

これとまったく同じ問題が、PHP の array データ構造の紛らわしさと組み合わせて、Drupal SA-CORE-2014-005 などの問題で悪用されるおそれがあります。悪用例を参照してください。

テンプレート言語

PHP は基本的にテンプレート言語です。しかし、既定では HTML エスケープを行いません。そのため、Web アプリケーションでの使用について非常に問題の多い言語となっています。下の「XSS」のセクションを参照してください。

その他の欠点

他にも、既定でオンである CSRF 対策メカニズムなど、Web フレームワークが提供するべき重要な機能があります。PHP には初歩的な Web フレームワークが付属しており、これが Web サイトの作成には十分実用的であるため、多くの人が CSRF 対策に必要な知識がないまま Web サイトを作成します。

サードパーティの PHP コード

前述の問題により、PHP で記述されたライブラリやプロジェクトは、往々にして危険です。特に、適切な Web フレームワークが使用されていない場合はなおさらです。一見無害なように見えるコードでもセキュリティの脆弱性が多く潜んでいる可能性があるので、Web 上で見つけた PHP コードは信用しないでください。

不完全に記述された PHP コードでは警告が発せられることが多く、問題を引き起こしかねません。一般的な解決策はすべての通知をオフにすることですが、これは適切な対応とは正反対であり (上記参照)、コードがさらに悪化します。

PHP の定期的な更新

稼働中のサーバー上の PHP のディストリビューションを定期的にアップグレードするように注意してください。毎日新しい脆弱性が PHP に見つかり、公表されています。攻撃者は任意のサーバー上のこうした新しい脆弱性をよく利用します。

設定

PHP の動作は、設定に大きく影響されます。設定は "php.ini" ファイル、Apache 設定ディレクティブ、およびランタイムメカニズムを通じて行うことができます (http://www.php.net/manual/en/configuration.php を参照)。

セキュリティに関連する設定オプションは多数あります。その一部を次に示します。

SetHandler

PHP コードは、'SetHandler' ディレクティブを使用して実行するように設定してください。多くの場合、誤って 'AddHander' ディレクティブを使用して設定されています。これでも動作しますが、他のファイルも PHP コードとして実行可能になります。たとえば、ファイル名 "foo.php.txt" が PHP コードとして扱われますが、このファイルが実行を意図したものでない場合や (サンプルコードなど)、悪意のあるファイルアップロードに由来するものである場合は、非常に深刻なリモート実行の脆弱性となるおそれがあります。

信頼できないデータ

ユーザー入力の結果または副産物であるデータはいずれも信頼してはいけません。データを適切な方法論によって検証するか、フィルター処理する必要があります。これで初めて、データが汚染されていないと見なすことができます。

信頼してはいけないスーパーグローバル変数は、$_SERVER$_GET$_POST$_REQUEST$_FILES、および $_COOKIE です。$_SERVER のすべてのデータがユーザーによる偽造が可能なわけではありませんが、その中のかなりの数が偽造可能です。特に HTTP ヘッダーを扱うデータ (HTTP_ で始まるもの) はすべて偽造可能です。

ファイルのアップロード

ユーザーから受け取ったファイルは、特にそれを他のユーザーがダウンロードできる場合は、さまざまなセキュリティ脅威の原因となります。特に、次のような脅威が考えられます。

  • HTML として提供されるファイルはすべて XSS 攻撃に利用される可能性があります。
  • PHP として扱われるファイルはすべて非常に重大な攻撃に利用される可能性があります (リモート実行の脆弱性)。

PHP は PHP コード (単に適切な拡張子を持つファイル) を非常に簡単に実行できる設計になっているため、PHP サイト (PHP がインストールされ設定されているすべてのサイト) では、アップロードされたファイルを保存する際に必ずファイル名をサニタイズすることが特に重要です。

$_FILES 配列の処理における一般的な誤り

次のコードに類似した処理を行うコードスニペットをインターネット上でよく見かけます。

   if ($_FILES['some_name']['type'] == 'image/jpeg') {  
       //ファイルを有効な画像として受け入れる処理に進みます
   }

しかし、ファイルタイプの判断は、タイプを検証するヒューリスティックを使用してではなく、クライアントが作成した、HTTP リクエストによって送信されたデータを単純に読み取ることによって行われています。完全とはいえませんが、より適切なファイルタイプの検証方法は、finfo クラスを使用するものです。

   $finfo = new finfo(FILEINFO_MIME_TYPE);
   $fileContents = file_get_contents($_FILES['some_name']['tmp_name']);
   $mimeType = $finfo->buffer($fileContents);

ここで、$mimeType はより適切に確認されたファイルタイプです。この方法はサーバーのリソースをより多く消費しますが、ユーザーが危険なファイルを送信し、コードをだましてそれを画像と信じ込ませるのを防止できます。画像ファイルは通常、安全なファイルタイプと見なされます。

$_REQUEST の使用

$_REQUEST は使用しないことを強くお勧めします。このスーパーグローバル変数をお勧めできない理由は、POST と GET のデータだけでなく、リクエストによって送信された Cookie もこの変数には含まれているからです。これらのデータすべてが 1 つの配列にまとめられているため、データのソースの特定がほぼ不可能です。これが混乱を招き、コードでミスを犯しやすくなります。そのため、セキュリティの問題につながるおそれがあります。

データベースに関するチートシート

SQL インジェクションの脆弱性が 1 つあれば、Web サイトのハッキングが可能になります。そのため、どのハッカーもまず SQL インジェクションの脆弱性を突こうとします。したがって、SQL インジェクションの脆弱性を修正することが、PHP ベースのアプリケーションの安全を確保するための第一歩となります。以下のルールを順守してください。

SQL ではデータの連結や補間を絶対に行わない

ユーザーデータを含む SQL 文字列の作成に、連結は使用しないでください。

   $sql = "SELECT * FROM users WHERE username = '" . $username . "';";

補間も使用しないでください。

   $sql = "SELECT * FROM users WHERE username = '$username';";

'$username' が信頼できないソースに由来する場合 (ソースコードでは簡単に確認できないため、実際そのように想定する必要があります)、'$username' に ' などの文字が含まれていて、意図とは大きく異なるクエリ (データベース全体の削除など) の実行を攻撃者に許す可能性があります。プリペアドステートメントとバウンドパラメーターを使用する方が優れた解決法です。PHP の [mysqli](http://php.net/mysqli) と [PDO](http://php.net/pdo) にこの機能が含まれています (下記参照)。

エスケープは安全ではない

mysql_real_escape_string は安全ではありません。SQL インジェクションの対策に関して、これを当てにしないでください。

理由: すべての変数で mysql_real_escape_string を使用し、それをクエリに連結する場合、一度くらいは忘れるに決まっています。その一度が命取りになります。絶対に忘れないようにすることは誰にも不可能です。また、SQL 内でも確実に引用符を使用しなければなりませんが、データを数値と仮定している場合などには、これは不自然な行為です。代わりに、プリペアドステートメントか、常に適切な SQL エスケープを実行してくれる同等の API を使用してください(ほとんどの ORM はこのエスケープを行うほか、SQL を作成してくれます)。

プリペアドステートメントを使用する

プリペアドステートメントは非常に安全です。プリペアドステートメントでは、データが SQL コマンドから分離されています。ユーザーが入力した内容すべてがデータと見なされ、元のままの状態でテーブルに格納されます。

PHP ドキュメントの「MySQLi prepared statements」と「PDO prepared statements」を参照してください。

プリペアドステートメントが機能しない場合

問題は、動的クエリを作成する必要がある場合や、プリペアド変数としてサポートされていない変数を設定する必要がある場合、またはデータベースエンジンがプリペアドステートメントに対応していない場合です。たとえば、PDO MySQL では ? を LIMIT 指定子としてサポートしていません。また、プリペアドステートメントは `SELECT` 文でテーブル名や列などにも使用できません。このような場合は、フレームワークでクエリビルダーが用意されていれば、それを使用します。クエリビルダーが用意されていない場合は、ComposerPackagist から複数のパッケージが利用できます。独自のものを使用しないでください。

ORM

ORM (オブジェクト関係マッピング) は適切なセキュリティ慣行です。PHP プロジェクトで ORM (Doctrine など) を使用していても、やはり SQL インジェクション攻撃を受ける可能性があります。ORM にクエリを挿入するのは通常よりはるかに困難ですが、ORM クエリを連結すると、SQL クエリを連結した場合と同じ脆弱性が生じてしまいます。したがって、データベースに送信する文字列は絶対に連結してないでください。ORM はプリペアドステートメントにも対応しています。

必ず、使用するすべての ORM のコードを検査して、そのコードで生成される SQL の実行がどのように処理されるかを確認してください。内部で値を連結するのではなくプリペアドステートメントが使用されていることや、適切なセキュリティ対策に準拠していることを確認します。

エンコードの問題点

やむを得ない場合を除き、UTF-8 を使用する

新しい攻撃ベクトルの多くはエンコードの迂回に基づいています。どうしても別のエンコードを使用しなければならない必要性がない限り、データベースおよびアプリケーションの文字セットには UTF-8 を使用してください。

   $DB = new mysqli($Host, $Username, $Password, $DatabaseName);
   if (mysqli_connect_errno())
       trigger_error("Unable to connect to MySQLi database.");
   $DB->set_charset('UTF-8');


その他のインジェクションに関するチートシート

SQL 以外にも、PHP には実行可能かつ一般的なインジェクションがいくつか存在します。

シェルインジェクション

次に示す少数の PHP 関数は、

文字列をシェルスクリプトやコマンドとして実行します。これらの関数に渡される入力 (特に、関数とは異なるバックティック (バッククォート) 演算子)。設定しだいでは、シェルスクリプトのインジェクションによってアプリケーションの設定や構成が漏えいしたり、サーバー全体が乗っ取られたりする可能性があります。これは非常に危険なインジェクションであり、攻撃者の天国と考えられています。

危険でないという絶対の確信がない限り (これにはホワイトリスト化が必須)、汚染された入力 (つまり、ユーザーによって何かしらの操作が行われた入力) を上記の関数に渡すことは絶対にしないでください。エスケープや他のどの対策も効果がありません。それぞれの対策を回避するためのベクトルが多数存在します。未熟な開発者の言うことは信じないでください。


コードインジェクション

PHP をはじめ、どのインタープリター言語にも、文字列を受け取ってそれを言語内で実行する関数が用意されています。PHP では、この関数を eval() といいます。 eval の使用は非常に悪い慣行です。セキュリティ面においてだけではありません。eval 以外に方法がないことに絶対の確信がある場合は、汚染された入力がまったくない状態で eval を使用します。また、eval は一般に実行が遅くなります。

preg_replace() 関数を、サニタイズされていないユーザー入力とともに使用しないでください。ペイロードが eval() で評価されます。

   preg_replace("/.*/e","system('echo /etc/passwd')");

リフレクションもコードインジェクションの欠陥を持つ場合があります。これは高度なトピックのため、リフレクションに関する適切なドキュメントを参照してください。

その他のインジェクション

LDAP、XPath、および文字列を実行する他のサードパーティアプリケーションは、インジェクションに対して脆弱です。文字列はデータでなくコマンドである場合もあるので、安全を確保してからサードパーティライブラリに渡す必要があることを常に念頭に置いてください。


XSS に関するチートシート

XSS に関しては次の 2 つのシナリオがあります。それぞれに適切な対策を講じる必要があります。

タグなし

ほとんどの場合、出力時には、エスケープされていない HTML タグをユーザー入力データに含める必要はありません。たとえば、テキストボックスの値をダンプするときや、セル内のユーザーデータを出力するときなどです。

テンプレートに標準 PHP を使用するか、`echo` などを使用している場合は、'htmlspecialchars' または次の関数 (実質的には 'htmlspecialchars' のより便利なラッパー) をデータに適用することで XSS を軽減できます。しかし、これはお勧めできません。問題は、それを毎回適用することを覚えておかなければならず、一度でも忘れたら XSS の脆弱性が生じるという点にあります。既定で安全でない方法論は、危険なものとして扱う必要があります。

この代わりとして、HTML エスケープを既定で適用するテンプレートエンジンの使用をお勧めします (下記参照)。HTML はすべてテンプレートエンジン経由で渡してください。

安全なテンプレートエンジンへの切り替えができない場合は、信頼できないすべてのデータに次の関数を使用します。

ユーザー入力を危険な要素 (style、script、image の src、a など) で使用する場合、このシナリオでは XSS を軽減できないので注意してください。ただし、これを行うことはまずありません。また、HTML タグを含めるつもりのない出力は、すべて次の関数でフィルタリングしてからブラウザーに送信する必要があることにも留意してください。

//XSS 対策用の関数
function xssafe($data,$encoding='UTF-8')
{
   return htmlspecialchars($data,ENT_QUOTES | ENT_HTML401,$encoding);
}
function xecho($data)
{
   echo xssafe($data);
}
//使用例
<input type='text' name='test' value='<?php  
xecho ("' onclick='alert(1)");
?>' />

信頼できないタグ

出力で使用する HTML タグの入力をユーザーに許可するが (リッチなブログコメント、フォーラム投稿、ブログ投稿など)、ユーザーを信頼できないときは、安全なエンコードライブラリを使用する必要があります。しかし、これは一般に困難で実行も遅くなります。ほとんどのアプリケーションに XSS の脆弱性があるのはこのためです。OWASP ESAPI には、データのさまざまな部分をエンコードするためのコード群が用意されています。また、OWASP AntiSammy や PHP 用の HTMLPurifier もあります。このいずれも適切に実行するためには多くの設定と学習が必要になりますが、良いアプリケーションを開発するにはこれらが欠かせません。

テンプレートエンジン

データの出力、およびほとんどの XSS 脆弱性からの防御においてプログラマーや設計者を支援するテンプレートエンジンがいくつか存在します。テンプレートエンジンの主眼はセキュリティではなく、設計エクスペリエンスの向上ですが、主要なテンプレートエンジンの大半は出力上の変数を自動的にエスケープし、エスケープしてはいけない変数がある場合は明示することを開発者に強制します。これにより、変数の出力がホワイトリストの性質を帯びます。このようなエンジンは複数存在しますが、その好例は twig[1] です。他にも一般的なテンプレートエンジンとして、Smarty、Haanga、Rain TPL が挙げられます。

手動でエスケープを適用したのでは、あまりにも忘れやすいため、XSS の適切な処理には、ホワイトリスト方式でエスケープするテンプレートエンジンが不可欠です。また、開発者はセキュリティを重視するならば、既定で安全なシステムを必ず採用する必要があります。

その他のヒント

  • どの Web アプリケーションにも信頼できるセクションを設けないでください。多くの開発者は管理者領域を XSS 対策の対象外にしますが、侵入者は管理者 Cookie や XSS に注目します。出力に変数が含まれている場合は、前述の関数を使用してすべての出力をクリアにする必要があります。echo、print、および printf のすべてのインスタンスをアプリケーションから削除して、安全なテンプレートエンジンに置き換えます。
  • HTTP-Only Cookie は、近い将来、すべてのブラウザーが対応したとき、非常に優れた慣行になります。今から使用を始めましょう(ベストプラクティスについては PHP.ini の設定を参照)。
  • 上に示した関数は、妥当な HTML 構文に対してしか機能しません。要素の属性を引用符で囲わないと痛い目に遭います。妥当な HTML を心掛けてください。
  • 反射型 XSS は普通の XSS と同様に危険であり、一般にアプリケーションの最も目立ちにくい場所に潜んでいます。それを探して軽減してください。
  • すべての PHP インストールで mhash 拡張機能が動作しているわけではありません。ハッシュ化を行う必要がある場合は、使用する前に確認してください。mhash が動作していない場合、SHA-256 ハッシュ化を実行できません。
  • すべての PHP インストールで mcrypt 拡張機能が動作しているわけでありません。動作していない場合は AES を実行できません。AES が必要な場合は確認してください。

CSRF に関するチートシート

CSRF 対策は理論上は簡単ですが、正しく実装するのは難しいといえます。まず、CSRF についてのヒントをいくつか示します。

  • 何か注目に値する操作を行うリクエストは、CSRF 対策が必要です。注目に値する操作とは、システムの変更と長時間かかる読み取りです。
  • ほとんどの場合、CSRF は GET で発生しますが、POST でも発生しやすいです。POST が安全とは絶対に思わないでください。

OWASP PHP CSRFGuard は、CSRF の対策方法を示すコードスニペットです。これをコピーして貼り付けるだけでは不十分です。近い将来、コピー & ペーストできるバージョンが入手可能になるでしょう (希望的観測ですが)。当面は、これと以下のヒントを組み合わせて対処します。

  • 危険な操作 (パスワード、復元用メールアドレスの変更など) には再認証を行う。
  • 行う操作が CSRF に耐性があるかどうか確信がない場合は、CAPTCHA の追加を検討します (ただし、CAPTCHA はユーザーにとっては不便です)。
  • (GET でも POST でもない) リクエストの他の部分、たとえば Cookie や HTTP ヘッダーなどに基づいて操作を実行する場合は、そこに CSRF トークンも追加することをお勧めします。
  • CSRF トークンの再作成には、AJAX ベースのフォームが必要です。これには前述の (コードスニペット内の) 関数を使用します。絶対に Javascript には頼らないでください。
  • GET や Cookie に対する CSRF は不便さにつながります。設計とアーキテクチャを検討してベストプラクティスを追求してください。

認証とセッション管理に関するチートシート

PHP には、すぐに利用できる認証モジュールは付属していません。独自に実装するか、PHP フレームワークを使用する必要があります。残念ながら、ほとんどの PHP フレームワークは、セキュリティの専門家ではなく、オープンソースの開発者コミュニティによって開発されているため、完璧からはほど遠いものです。次に、有益で役に立つヒントをいくつか示します。

セッション管理

PHP の既定のセッション機能は安全と考えられています。生成される PHPSessionID は十分にランダムですが、保存は必ずしも安全とはいえません。

  • セッションファイルは一時フォルダー (/tmp) に保存されるため、suPHP をインストールしない限り、全世界から書き込み可能です。したがって、LFI やその他の漏えいによって操作されるおそれがあります。
  • 既定の設定では、各セッションがファイルに保存されますが、アクセスが多い Web サイトでは、これが非常に遅くなります。代わりに、メモリフォルダーにセッションを保存できます (UNIX の場合)。
  • PHP に頼ることなく、独自のセッション機構を実装できます。その場合は、セッションデータをデータベースに保存してください。必要に応じて、PHP のセッション処理機能の全部または一部を使用しても、1 つも使用しなくてもかまいません。

セッションハイジャック対策

セッションを IP アドレスにバインドするのは適切な慣行です。これにより、ほとんどのセッションハイジャックのシナリオを阻止できます (ただし、すべては阻止できません)。しかし、中には匿名化ツール (Tor など) を使用しているユーザーがいるかもしれません。こうしたユーザーはサービスで支障をきたします。

これを実装するには、単純に、セッションの初回作成時にクライアントの IP をセッションに保存し、以降は強制的に同じになるようにします。次のコードスニペットはクライアントの IP アドレスを返します。

$IP = getenv ( "REMOTE_ADDR" );

ローカル環境では、有効な IP が返されないことに留意してください。通常は :::1 または :::127 という文字列が返されるので、IP チェックロジックを調整します。また、このコードで HTTP_X_FORWARDED_FOR 変数を使うバージョンには気を付けてください。このデータは実際にはユーザー入力であり、スプーフィング攻撃を受けやすいからです (詳細については こちらこちらを参照)

セッション ID の無効化

違反 (たとえば、IP アドレスが 2 つ観測されるなど) が発生するたびにセッションを無効化してください (Cookie の解除、セッション保存の解除、トレースの削除)。ログイベントが役に立つ場合があります。多くのアプリケーションでは、ログインユーザーも通知します (GMail など)。

セッション ID のロール

昇格が発生するたびにセッション ID をロールしてください。たとえば、ユーザーがログインしたら、セッションの重要度が変わるので、そのセッション ID を変更する必要があります。

セッション ID の露出

セッション ID は守秘性が高いと考えられます。アプリケーションがセッション ID をどこにも露出しないようにしてください (特にログインユーザーにバインドされている場合)。セッション ID の媒体として URL を使用しないでください。

セッションが機密情報を含む場合は、必ず TLS 経由でセッション ID を転送します。そうしないと、待ち伏せ攻撃者がセッションハイジャックを実行できます。

セッションの固定

ユーザーのログイン後 (または各リクエスト後) に session_regenerate_id() を使用してセッション ID を無効化してください。

セッションの有効期限切れ

一定のアイドル時間の経過後および一定の活動時間の経過後にセッションを有効期限切れにする必要があります。有効期限切れ処理とは、セッションを無効化して削除し、別のリクエストが来たら新しいセッションを作成することを意味します。

また、[ログアウト] ボタンを閉じ、ログアウト時にそのセッションのすべてのトレースを解除します。

アイドル時間タイムアウト

現在のリクエストが前回のリクエストから X 秒経過している場合、セッションを有効期限切れにします。これには、リクエストが行われるたびにセッションデータのリクエスト時刻を更新する必要があります。一般的な設定時間は 30 分ですが、アプリケーションの条件に大きく左右されます。

この有効期限切れは、パブリックにアクセス可能なマシンにログインしたユーザーがログアウトし忘れたときに役立ちます。セッションハイジャックにも有効です。

一般タイムアウト

現在のセッションが一定期間にわたって活動状態が続いた場合、活動状態であってもセッションを有効期限切れにします。これにより状況を把握しやすくなります。期間はまちまちですが、通常は 1 日から 1 週間程度でかまいません。これを実行するには、セッションの開始時刻を格納する必要があります。


Cookie

PHP スクリプトで Cookie を処理するには、いくつかのこつがあります。

シリアル化しない

Cookie に格納されたデータを絶対にシリアル化しないでください。簡単に操作されて、スコープに変数を追加されるおそれがあります。

適切な削除

Cookie を安全に削除するには、次のスニペットを使用します。

setcookie ($name, "", 1);
setcookie ($name, false);
unset($_COOKIE[$name]);

1 行目でブラウザー内の Cookie を確実に有効期限切れにします。2 行目は Cookie を削除する標準的な方法です (false は Cookie に格納できません)。3 行目で Cookie をスクリプトから削除します。多くのガイドでは time() - 3600 を使用して有効期限切れにするよう開発者に勧めていますが、これはブラウザーの時刻が正しくない場合に動作しない可能性があります。

また、session_name() を使用すると、既定の PHP セッション Cookie 名を取得できます。

HTTP Only

最新のブラウザーのほとんどは、HTTPOnly 属性付き Cookie に対応しています。これらの Cookie は HTTP(s) リクエストを介してのみアクセス可能で、JavaScript からはアクセスできません。したがって、XSS スニペットからもアクセスできません。これは非常に優れた慣行ですが、十分とはいえません。というのも、主要なブラウザーで、JavaScript への HTTPOnly 属性付き Cookie の漏えいにつながる欠陥が数多く見つかっているからです。

HTTPOnly 属性付き Cookie を PHP (5.2 以上) で使用するには、セッション Cookie 設定を次のように手動で行う必要があります (session_start は使用しません)。

#prototype
bool setcookie ( string $name [, string $value [, int $expire = 0 [, string $path [, string $domain [, bool $secure = false [, bool $httponly = false ]]]]]] )
#usage
if (!setcookie("MySessionID", $secureRandomSessionID, $generalTimeout, $applicationRootURLwithoutHost, NULL, NULL,true))
    echo ("could not set HTTP-only cookie");

path パラメーターは、Cookie を有効にする対象のパスを設定します。たとえば、あなたの Web サイトが example.com/some/folder にある場合は、パスを /some/folder に設定します。これを設定しないと、example.com にある他のアプリケーションもあなたの Cookie を参照するおそれがあります。ドメイン全体を使用している場合は、気にしなくてかまいません。Domain パラメーターはドメインを強制します。複数のドメインまたは IP からアクセス可能な場合は、これを無視してください。それ以外の場合は適切に設定します。secure パラメーターを設定すると、Cookie は HTTPS 経由でのみ送信可能になります。次の例を見てください。

$r=setcookie("SECSESSID","1203j01j0s1209jw0s21jxd01h029y779g724jahsa9opk123973",time()+60*60*24*7 /*a week*/,"/","owasp.org",true,true);
if (!$r) die("Could not set session cookie.");

Internet Explorer の問題点

傾向として Internet Explorer の多くのバージョンには Cookie に関する問題があります。たいていは、有効期限を 0 に設定することで問題が解消します。

認証

自動ログイン (Remember Me) 機能

多くの Web サイトは自動ログイン機能に脆弱性があります。正しい手法は、ユーザーに対して 1 回限りのトークンを生成し、それを Cookie に保存することです。このトークンを検証してユーザーに割り当てるためには、このトークンがアプリケーションのデータストアにも存在しなければなりません。このトークンはユーザー名やパスワードと無関係であることが必要で、安全で十分に長いランダムな数値が適切です。

自動ログイン用トークンに対するブルートフォース攻撃を防ぐためには、これをお勧めします。また、トークンには十分な長さを確保してください。そうしないと、攻撃者が自動ログイン用トークンに対してブルートフォース攻撃を仕掛け、やがては資格情報なしでログインユーザーにアクセスする可能性があります。

  • ユーザー名 / パスワードやそれに関連する情報を Cookie に保存することは絶対にしないでください。

設定とデプロイに関するチートシート

PHP Configuration Cheat Sheet」を参照してください。

Authors and Primary Editors

Abbas Naderi Afooshteh (abbas.naderi@owasp.org)

Achim - Achim at owasp.org

Andrew van der Stock

Luke Plant

Other Cheatsheets

OWASP Cheat Sheets Project Homepage

Developer Cheat Sheets (Builder)

Assessment Cheat Sheets (Breaker)

Mobile Cheat Sheets

OpSec Cheat Sheets (Defender)

Draft Cheat Sheets