PHPのセキュリティ対策
※当サイトにはプロモーションが含まれています。
ユーザーによる入力値の検証
- mb_check_encoding関数で文字エンコーディングが正しいかチェックする。
- 制御文字を入力不可としてよい場合は、正規表現等でチェックする。
クロスサイトスクリプティング(XSS)対策
1) HTML テキストの入力を許可しない場合の対策
-
表示の際に文字列をHTMLエスケープすることを徹底する。
-
信頼できない値を出力する場所(コンテキスト)によって、エスケープ方法が異なる。
置かれている場所 説明 エスケープの概要 要素内容(通常のテキスト) タグと文字参照が解釈される。「<」で終端 「<」と「&」を文字参照に 属性値 文字参照が解釈される。引用符で終端 属性値を「”」で囲み、「<」と「”」と「&」を文字参照に 属性値(URL) 同上 URLの形式を検査してから属性値としてのエスケープ イベントハンドラ 同上 JavaScriptとしてエスケープしてから属性値としてのエスケープ script要素内の文字列リテラル タグも文字参照も解釈されない。「</」により終端 JavaScriptとしてのエスケープおよび「</」が出現しないよう考慮
要素内容(通常のテキスト)
このコンテキストの例
<div> ...ここに出力したい... </div>
エスケープ方法
htmlspecialchars($str, ENT_QUOTES, $charset)
- 第三引数をちゃんと指定すること。但し、PHP 5.6.0 以降では、デフォルト値として default_charset の値が使用される。
- 参考:PHP: htmlspecialchars - Manual
属性値
このコンテキストの例
<input id="foo" name="foo" value="...ここに出力したい... "/>
- 文字参照が解釈されることに注意する。
エスケープ方法
- 必ずダブルクォートで囲む。
- その上で以下のようにエスケープする。
htmlspecialchars($str, ENT_QUOTES, $charset)
属性値(URL)
href や src 属性に指定する値
このコンテキストの例
<a href="...ここに出力したい... "/>foo</a>
<iframe src="...ここに出力したい... "/>
- エスケープ処理と共に、ドメインのチェックも必要。
- 文字参照が解釈されることに注意する。
エスケープ方法
-
URL Scheme をチェックする。
- 例えば、”http” or “https” or “/” が先頭にあればOKとする。
-
URLの各パラメータ値に信用できない値を指定する場合は、この値の部分を URLエンコードする。
$param1 = urlencode($_POST['param1']); $url = "http://example.com/?param1=" . $params1; -
URL全体を以下のようにHTMLエスケープする。
$param1 = urlencode($_POST['param1']); $param2 = urlencode($_POST['param2']); $url = "http://example.com/?param1=" . $params1 . "&" . $param2; $url_escaped = htmlspecialchars($url, ENT_QUOTES, $charset); // この $url_escaped を href や src の値としてセットする。 -
属性値(URL)のエスケープで文字列はどう変化するか?の例を以下に示す。
// 以下のURLを aタグのsrc属性値に指定したい場合(ageパラメータ値はユーザーが指定したとする) $url = "http://www.example.com/?name=taro&age=<script>alert(1)"; // // まずユーザが入力したパラメータ値をurlencodeする。 // $url1 = "http://www.example.com/?name=taro&age=" . urlencode("<script>alert(1)"); // // 次のように、パーセントエンコーディングされる。 // ↓ // http://example.com/?name=taro&age=%3Cscript%3Ealert%281%29%3C%2Fscript%3E // 次にURL全体をHTMLエスケープする // $url2 = htmlspecialchars($url1, ENT_QUOTES, 'UTF-8'); // // すると、&が文字参照の&になる。これを aタグのsrc属性値として指定すれば良い。 // ↓ // http://example.com/?name=taro&age=%3Cscript%3Ealert%281%29%3C%2Fscript%3E
イベントハンドラ属性値
このコンテキストの例
<div onmouseover="func('...ここに出力したい...')">検知したら実行させない</div>
- 信頼できない値を出力するのは、クォートで囲まれた文字列リテラル内に限定する。それ以外は危険。
- 文字参照が解釈されることに注意する。
エスケープ方法
- JavaScriptの文字列リテラルにおいて、エスケープシーケンスでの表現が必要な文字はそれに従った記述を行う。
- 例えば、ダブルクォート文字やシングルクォート文字を表すには、その前にバックスラッシュを付加する必要がある。
- 参考:7 Lexical Conventions # Ⓣ Ⓔ ① Ⓐ — Annotated ES5
- HTMLエスケープする。
- 属性値としてダブルクォートで囲む
エスケープ方法(Unicodeエスケープを使う方法)
- 文字列リテラルに出力する値をエスケープする1つの方法は、英数文字以外を Unicodeエスケープしてしまうことである。
- この方法であれば、HTMLタグの文字列があったとしても、HTMLとして解釈されることはなく、あくまでJavaScriptの中でエスケープ前の文字列として使用されるだけになる。つまりこれだけやれば文字列を埋め込める。
JavaScriptの文字列リテラルを生成する関数の例
-
体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践 に掲載されていたスクリプトを少し変更してある。詳細はこちらの書籍を参考にして欲しい。
-
JavaScriptのエスケープ方法が複雑なため、1つ1つきめ細かいエスケープをするのではなく、数値とアルファベット(と「-」、「.」も)以外はざっくりとUnicodeエスケープしている。
function escape_js_string($s) { return preg_replace_callback('/[^-\.0-9a-zA-Z]+/u', function($matches){ $u16 = mb_convert_encoding($matches[0], 'UTF-16'); return preg_replace('/[0-9a-f]{4}/', '\u$0', bin2hex($u16)); }, $s); }
scriptタグ内
このコンテキストの例
<script>var foo="...ここに出力したい... ";</script>
- 信頼できない値を出力するのは、クォートで囲まれた文字列リテラル内に限定する。それ以外は危険。
エスケープ方法
- JavaScriptの文字列リテラルにおいて、エスケープシーケンスでの表現が必要な文字はそれに従った記述を行う。
- 例えば、ダブルクォート文字やシングルクォート文字を表すには、その前にバックスラッシュを付加する必要がある。
- 参考:7 Lexical Conventions # Ⓣ Ⓔ ① Ⓐ — Annotated ES5
- HTMLエスケープする。
</scriptがある場合は、<\/scriptに変換する。
参考
エスケープ方法(Unicodeエスケープを使う方法)
- 文字列リテラルに出力する値をエスケープする1つの方法は、英数文字以外を Unicodeエスケープしてしまうことである。
- この方法であれば、
</もエスケープされるのでスクリプトが終端される心配はないし、HTMLエスケープもする必要がない。 - 但し、この方法でエスケープした文字列を setAttributeメソッド等で、イベントハンドラ属性に指定すると JavaScriptコードとして実行されてしまうので注意する。(参考: DOM based XSS Prevention Cheat Sheet - OWASP)
- この方法であれば、
JavaScriptの文字列リテラルを生成する関数の例
- 「イベントハンドラ属性値のエスケープ」に書いたものと同様。
JavaScriptに値を渡す方法
- JavaScriptの文字列リテラルに値を直接埋め込むのではなく、間接的に値を渡す方が安全である。
データセット属性を使う方法
-
HTMLタグの属性を
data-xxx="{{ 信用できない値 }}"というように生成しておき、JavaScriptから取得させる。PHP側で値をセットする
<div id="foo" data-bar="<?php echo htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); ?>"/>JavaScript側で値を取り出す
var value = document.querySelector('#foo').dataset.bar; // もしくは var value = document.querySelector('#foo').getAttribute('bar');
hiddenパラメータを使う方法
-
inputタグのtype属性にhiddenを指定して値をセットし、JavaScriptから取得させる。
PHP側で値をセットする
<input type="hidden" id="foo" value="<?php echo htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); ?>"/>JavaScript側で値を取り出す
var value = document.querySelector('#foo').value;
参考
2) HTML テキストの入力を許可する場合の対策
- HTMLの危険な記述部分を削除してくれるライブラリを使う。
- HTML Purifier - Filter your HTML the standards-compliant way!
- http://htmlpurifier.org/
- HTML Purifier の使い方 - Secure Code Tips
- HTML Purifier - Filter your HTML the standards-compliant way!
3) 全てのウェブアプリケーションに共通の対策
-
文字コードの指定
-
php.iniのdefault_charsetを指定する。(これにより、HTTP レスポンスヘッダの Content-Type フィールドに文字コードがセットされる)
php.ini
default_charset = "UTF-8"- PHP 5.6.0 以降は “UTF-8” がデフォルトになっている。
- すべてのバージョンの PHP は、PHP から送信する Content-Type ヘッダのデフォルト値としてこの設定値を使う。(ただし、header() で上書きは可能)
- 参考:PHP: コア php.ini ディレクティブに関する説明 - Manual
-
meta要素を記述する。
HTMLファイルのヘッダ部分
<meta charset="UTF-8">
-
-
HTMLタグの属性値はダブルクォートで囲む
-
セッションクッキーに HttpOnly属性を設定する。
ini_set('session.cookie_httponly', 1); -
HTTPレスポンスヘッダに「X-XSS-Protection」を設定して XSS攻撃を検知させる。 例
// 検知したら実行させない header("X-XSS-Protection: 1; mode=block"); -
HTTPレスポンスヘッダに「Content-Security-Policy」を設定する。 例
// JavaScriptの実行を許可する対象を 同一オリジンと code.jquery.com と maxcdn.bootstrapcdn.com に制限する header("Content-Security-Policy: default-src 'self'; script-src 'self' code.jquery.com maxcdn.bootstrapcdn.com");
参考
- XSS (Cross Site Scripting) Prevention Cheat Sheet - OWASP
- DOM based XSS Prevention Cheat Sheet - OWASP
SQLインジェクション対策
- エンコーディングの指定
- プレースホルダの利用
- データベースに接続するユーザの権限を限定する。
PDOを使う場合
-
DBへの接続と設定
// charset は set names でセットせず、この第1引数で指定すること(PHP5.3.6以降で可能) $db = new PDO('mysql:host=localhost;dbname=foo;charset=utf8', 'username', 'password', array( // 静的プレースホルダを指定 PDO::ATTR_EMULATE_PREPARES => false, // エラー発生時に例外を投げる PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION )); -
参考
(仕方なく)生のSQL文を書く場合の例
- 信用できない値を埋め込む場合は、以下を参考にしてちゃんとエスケープ処理する。
エスケープ方法
-
整数リテラルには intval関数を通す。
- PDO::quote メソッドの第2引数に PDO::PARAM_INT を指定すしてエスケープすればよさそうだが、これには実は問題があるため使わない。(「参考」のリンク先を参照)
-
文字列リテラルは PDO::quote メソッドでエスケープして、シングルクォートで囲む。
- 他のデータベース抽象化ライブラリにも大抵は似たようなメソッドがある。
- データベース毎に用意されたエスケープ用関数(例えば MySQLなら mysqli_real_escape_string 関数)でもよい。
例
$item_id = intval($_POST["item_id"]); $name = $db->quote($_POST["name"], PDO::PARAM_STR); $result = $dbh->exec( "INSERT INTO items (item_id, name)". "VALUES($item_id, $name)");
参考
- 『最初に「読む」PHP』は全体的にとても良いが惜しい脆弱性がある - 徳丸浩の日記
- [Perl][PHP][SQL]: quoteメソッドの数値データ対応を検証する - 徳丸浩の日記(2009-10-19)
クロスサイトリクエストフォージェリ(CSRF)対策
- トークンを埋め込んで接続元を判定する。
- トークンの生成には、暗号論的擬似乱数生成器を使用する。
- PHP5.3.0以降であれば、openssl_random_pseudo_bytes関数を使うとよい。
- ワンタイムトークンを使う必要はない。
OSコマンド・インジェクション対策
- なるべく、外部からのパラメータ値をOSコマンド(パラメータを含む)に使用しない。
- 外部からは番号等を指定させ、実際にコマンドに使用する文字列は予め用意された文字列を使用する。
- パラメータのエスケープには、escapeshellarg関数を利用する。
ディレクトリ・トラバーサル対策
-
できれば、ファイル名を外部から指定させない。
-
basename関数を利用する。
- 但し、basename関数はヌルバイトを削除しないので自分で削除する必要がある。もしくは、逆にホワイトリスト方式で許可する文字だけで構成されているかチェックする。
- また basename()は setlocale()がちゃんと設定されている必要がある。
注意: basename() はロケールに依存します。 マルチバイト文字を含むパスで正しい結果を得るには、それと一致するロケールを setlocale() で設定しておかなければなりません。
メールヘッダインジェクション対策
- できれば、外部からのパラメータ値をメールヘッダに埋め込まないようにする。
- 外部から指定するメールアドレスをバリデーションする。
- メールアドレスのバリデーション:PHP: 検証 - Manual
- 件名のバリデーション:「制御文字以外にマッチする」正規表現を利用する。
参考
- mb_send_mail(),mail()で第5引数を設定する際の注意点 - t_komuraの日記
HTTPヘッダ・インジェクション対策
- header関数を使えばよさそう。
- 但し、リダイレクトさせる場合はドメインをチェックする。
メモ
- PHP5.1.2から、header()関数は一度に複数のヘッダを送信できないようになった。 これは、ヘッダインジェクション攻撃への対策のためである。
- 参考
オープン・リダイレクト対策
リダイレクトには以下の3パターンがある。
-
レスポンスヘッダ(“Location: URL”)でリダイレクトさせる
例
header('Location: http://www.example.com/'); -
を使ってリダイレクトさせる
例
<meta http-equiv="Refresh" content=0;URL=http://www.example.com/'"> -
JavaScriptによるlocationオブジェクトへの代入によってリダイレクトさせる
例
location.href = url_from_input;
対策
- リダイレクト先のURL文字列に、ユーザーの入力した値を直接使用しない作りにする。ユーザーの入力した値を基に、アプリケーション側で用意した文字列を選択して使用する。
- それができない場合
- URL Scheme をチェックする(http もしくは https のみ許可するなど)
- 許可されたドメインかどうかチェックする。
- は危険なので使わないほうが良い。
参考
- HTML5時代の「新しいセキュリティ・エチケット」(4):これなら合格! 正しいリダイレクターの作り方 (1/3) - @IT
セッション管理の不備への対策
セッションフィクセーション
- ログイン時に、session_regenerate_id() を実行してセッションIDを再生成する。
ファイルアップロード
- アップロードされたファイルを公開ディレクトリに置かない。
- 画像を扱う場合、BMP形式はプログラムで扱い辛い面があるため対象外にしておくのが妥当である。
- IE7以前での画像XSS対策
- イメージファイトのまとめ: 画像ファイルによるクロスサイト・スクリプティング(XSS)傾向と対策 - 徳丸浩の日記(2007-12-10)
画像ファイルの判定について
- FileInfo の関数や、getimagesize関数で判定する。これらはマジックバイトでの判定であり、多少信頼性が低いので imagecreatefromstring 関数でイメージリソースが生成できるか確認しておくとよい。
- exif_imagetype関数でも画像の種類は判定はできるが、exif 拡張モジュールを必要とする。
渡されたファイル(ファイルパス)が画像ファイルであるかどうかをチェックする関数の例
- set_error_handler関数を使って、全てのエラーで ErrorExceptionクラスをスローしている環境を想定している。
- imagecreatefromstring関数は環境によって対応している画像フォーマットが違ってくるらしいので、対応している画像フォーマットのみに使用する。
/**
* @param String $filepath ファイルパス(拡張子は当てにしない)
* @return bool
*/
function isValidImageFile($filepath)
{
try {
// WARNING, NOTICE が発生する可能性あり
$img_info = getimagesize($filepath);
switch ($img_info[2]) {
case IMAGETYPE_GIF:
case IMAGETYPE_JPEG:
case IMAGETYPE_PNG:
// イメージリソースが生成できるかどうかでファイルの中身を判定する。
// データに問題がある場合、WARNING が発生する可能性あり
if (imagecreatefromstring(file_get_contents($filepath)) !== false) {
return true;
}
}
} catch (\ErrorException $e) {
// ログ出力する文字列の例
$err_msg = sprintf("%s(%d): %s (%d) filepath = %s",
__METHOD__, $e->getLine(), $e->getMessage(), $e->getCode(), $filepath);
// TODO:
// - $e->getSeverity() の値によって、ログ出力を変えたりする。
}
return false;
}
メモ
- JPEG, TIFF の場合は、EXIF情報を削除することも検討する。
参考
- PHP/Apache httpdのファイルアップロード/ダウンロード処理 - yohgaki’s blog
パスワードの保存方法
- PHP5.5以降が使える環境では password_hash関数を使う。
アクセス制御や認可制御の欠落
- 権限情報はセッション変数に保持して、権限が必要な処理の直前で必要な権限をチェックする。
PHPの設定値
開発環境用の設定
display_errors = On
display_startup_errors = On
error_reporting = -1
log_errors = On
全てのエラーを表示する設定(PHPのバージョン毎)
< 5.3 -1 or E_ALL
5.3 -1 or E_ALL | E_STRICT
< 5.3 -1 or E_ALL
本番環境用の設定
display_errors = Off
display_startup_errors = Off
error_reporting = E_ALL
log_errors = On
その他のメモ
- ereg関数はバイナリセーフでない。このためPHP 5.3.0 で非推奨となった。
- アプリケーション開発者は Composer を使うことで、require / require_once / include / include_once に起因するファイルインクルード攻撃についてはあまり心配しなくてよくなった(これらを直接使うことはなくなったので)。
- eval は使わない。
- 本記事では、IE7以前に実装されていた「CSS Expressions」機能については触れていない。
- 「外部からコントロールできる値をunserialize関数に処理させない」
参考
- PHP: セキュリティ - Manual
- 体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践
- 安全なPHPアプリケーションの作り方2014
- 「10日でおぼえるPHP入門教室 第4版」はセキュリティ面で高評価 - 徳丸浩の日記」の内容
- PHP: The Right Way - Security
- PHP と Web アプリケーションのセキュリティについてのメモ
- OWASP Secure Coding Practices – Quick Reference Guide - yohgaki’s blog