PHP文字列をテキストとして出力したい場合もあります。PHPの文字列型はバイナリセーフなのでどのようなデータでも保存可能ですが、プログラム中でPHP変数をPHPのテキスト(リテラル)として出力するにはaddslashes()によるエスケープ処理が必要です。
【重要】エスケープ/API/バリデーション1は出力先に合った方法でないと意味がないです。一口にHTMLと言っても複数の”コンテクスト”があります。
- JavaScript(識別子、変数など)、CSS、タグ属性名、タグ属性値(URIコンテクストに特に注意。BASE64、JavaScriptを使う場合もある)
があります。
SQLクエリと言っても
- 引数(更にLIKE、正規表現、JSON、XMLなどに別れる)、識別子、SQL語句
などがあります。全てのテキストインターフェースにコンテクスト2があります。
それぞれの”コンテクスト”に適切なエスケープ/API/バリデーションを利用しなければ意味がありません。
【追記】 PHPでのエスケープ方法を検索する為に参照する方が多いようです。ここでは”PHPの文字列データ”をエスケープする方法を解説します。まず他のエスケープ方法など、恐らく検索目的である可能性が高いエントリを紹介します。
セキュリティ専門家と言われる人でも誤解している場合がありますが、エスケープやAPIを使った出力処理は第一のセキュリティ対策ではありません。セキュアコーディングの第一原則は入力バリデーションです。出力の無害化(エスケープ/API利用/バリデーション)はずっと下の七番目の原則です。
入力値の種類は三種類しかない
キッチリと不正な入力を廃除することは、コードが正しく動作する為の前提条件です。不正な入力値の廃除ができていないと、正しく動作するシステムの構築が困難になります。(後述 ー 「出力対策だけのセキュリティ設計」が誤りである理由を参照)
入力と出力処理は独立した処理3です。出力時には基本的には入力バリデーションの有無に関わらず全ての出力データを安全にエスケープ、安全に出力可能なAPIで無害化、または無害化を保証するバリデーションを行います。
値の種類が3種類しかない事は、出力対策時のバリデーションにも適用できます。出力時も上の図の赤い部分、不正な値、を完全に廃除するバリデーションを行います。4
出力対策(出力無害化)の三原則
出力を無害化する方法には3つの方法があり、原則として出力の無害化を行う場合にはこの3つ方法、エスケープ/API/バリデーション、の1つ以上を使う必要があります。
エスケープと出力対策
特定の出力処理に於て「エスケープは要らない、知る必要もない」という意見を時々耳にします。エスケープとは何か?を知ればそうは考えないと思います。
出力対策だけでは正しく動作するマトモなアプリケーションは作れません。第一のセキュアコーディング原則(入力をバリデーションする)が必要です。5 6
バリデーション
出力対策にもバリデーションが欠かせません。また、出力対策だけではセキュアなアプリケーションは作れません。入力対策も必要です。入力対策はセキュアなソフトウェア構築の第一原則ですが、出力対策と同じように、入力対策”だけ”でもセキュアなアプリケーションは作れません。入力対策と出力対策、両方が必要です。(+当然ながら、正しいプログラムロジックも必要)
バリデーションとは何か?は次のエントリを参考にしてください。
アプリケーションレベルの出力でバリデーションが利用できる場合、可能な限りバリデーションする方が良いです。
エスケープ方法
エスケープ方式、あれこれです。重要なので繰り返しますが出力コンテクストに合ったエスケープ/APIを利用しないと意味がありません。
JavaScript文字列
JavaScript文字列コンテクストにはJavaScript文字列用のエスケープが必要です。JavaScript文字列はHTML文書、JavaScript、JSON、SVGなどの中に現れます。JavaScriptの文字列変数をサーバー側で作る場合に利用します。
URIコンテクストはjavascript:<JavaScriptのコード>といった書き方ができます。この場合、<JavaScriptのコード>の中のJavaScript文字列が変数の場合はこのエスケープが必要です。
HTMLコンテクストの文字列
HTML文書の要素となる文字列にはHTMLエスケープが必要です。HTMLに出力するから、といって何でもHTMLエスケープすればOKではない点に注意してください。
CSSコンテクストの文字列
今のブラウザはCSS定義中のスクリプト実行ができないのでCSS”ファイル”ならコードインジェクションはできません。HTMLコンテクスト中にCSSを出力する場合、適切にエスケープしないとコードをインジェクションされます。どちらのコンテクストでもエスケープしないとクリックジャックで攻撃可能になるのでエスケープが必須です。
URI(URL)コンテクストの文字列
URIコンテクストのエスケープはまだ書いていません。以下の何れかを使ってエスケープします。よくある間違いはURIコンテクスト(src属性値など)なのにHTMLエスケープを利用してしまう事です。パラメーター名にもURIエスケープが必要です。
URIコンテクストにはdata:やjavascript:も利用できます。それぞれBASE64、JavaScript文字列のエスケープが必要になります。
SQLコンテクストの文字列
現在、最もよくあるパターンのSQLインジェクション脆弱性は識別子のエスケープ漏れです。特定カラムでソートしたい、特定のカラムのみ抽出したい、といった場合に漏れていることがよくあります。
SQLクエリを経由したインジェクション攻撃を完全に防ぐには、様々なコンテクストを意識してエスケープ/バリデーションする必要があります。
PostgreSQLのエスケープ
- pg_escape_literal() – 文字リテラル(SQLパラメーター)のエスケープ
- pg_escape_string() – 文字リテラルの一部となる変数をエスケープ
- pg_escape_identifier() – 識別子のエスケープ
- pg_escape_bytea() – バイナリ型のエスケープ
MySQLのエスケープ
- mysqli::real_escape_string() – 文字リテラルのエスケープ
- MySQLのAPIには識別子エスケープのAPIがありません。手動でエスケープする必要があります。ANSI SQLモード/非ANSI SQLモードでクオート/エスケープ方法が異なるので注意してください。
SQLiteのエスケープ
- SQLite3::escapeString() – 文字リテラルのエスケープ。
- SQLite3には識別子エスケープのAPIがありません。手動でエスケープする必要があります。SQLiteは標準SQL、MySQL、MS Access形式をサポートしています。形式の違いに注意してください。通常は標準SQL方式を使うと良いでしょう。(pg_escape_identifier()が流用できそうに見えますが、PostgreSQLの接続がない場合、文字エンコーディングのバリデーションができないので無条件にはオススメできません)
※ SQL標準の識別子(カラム名/テーブル名など)のクオートは ” (ダブルクオート)です。エスケープには ” を使います。例: “My Column with “” works” MySQLのTraditionalモードでは動作しないです。標準クオートを利用するにはANSIモードが必要です。
LDAPコンテクストの文字列
LDAPクエリパラーメーターにはDNとFilterの2つのコンテクストがあります。それぞれ適切な方式でエスケープしなければなりません。SQLクエリ同様、識別子/LDAPクエリ語句にも注意が必要です。LDAP識別子にはエスケープ方法が定義されていません。これらにはバリデーションが必要です。
OSコマンドコンテクストの文字列
OSコマンドパラメーターのエスケープには信頼性がありません。これはコマンドパラメーターがコマンド/シェルによってどのように処理されるか分らないからです。バリデーションした後、エスケープする、といった対策が必要です。
JSONデータ
JSONデータはHTML/JavaScript/JSONコンテクストに出力される可能性があります。JSONデータをエスケープする場合、どのコンテクストに出力しても無害である方式でエスケープします。
XPath 1.0クエリ
XPath 1.0クエリ(PHPが利用するlibxml2がサポートするXPath)がサポートする文字列パラメータにはエスケープが定義されていません。壊れた仕様、と言えますが少し工夫すると”エスケープ”できます。XPath 2.0はSQLクエリと同様なエスケープ方式が定義されています。
正規表現文字列のエスケープ
正規表現文字列にもエスケープが必要です。正規表現にパラメーターを許すと簡単にReDoSに脆弱になったり、正規表現インジェクションに脆弱になります。
- preg_quote() – PCRE用の正規表現エスケープ
残念ながらmbregex用のエスケープAPIはありません。手動でエスケープします。
C言語文字列のエスケープ
PHPはC言語で作られています。拡張モジュールやライブラリもC言語で作られています。このため、C言語文字列用のエスケープが必要になる場合があります。
- addcslashes() – C言語用の文字列エスケープを行う
その他の出力
全ての出力はその出力先コンテクストに対して無害でなければなりません。エスケープAPI/エスケープが不必要なAPIが無くても、エスケープ可能な場合にエスケープします。
エスケープやAPI(本当に変数の無害化が可能な物に限る)が無害化の手法として役立ちますが、コンテクストによってはエスケープもエスケープが不必要なAPIも使えない場合があります。この場合、バリデーションし不正なデータは致命的なエラーとして処理するしかありません。ただし、出力時のバリデーションは「遅すぎる」のでDoS問題の原因になります。入力処理時のバリデーションが重要です。
今のPHPのheader関数およびsetrawcookie関数は改行を許さないので、新しい不正なHTTPヘッダーを挿入したり、HTTPヘッダーを分割してHTTPコンテンツにすることは出来ません。しかし、内容は自由に変えられます。HTTPヘッダーの内容が正しい内容であることを保障するにはバリデーションするしかありません。
HTTPヘッダーと同じような構造を持つメールの場合も同様です。
出力先にとって一文字でも意味がある場合、出力のサイズが決まっている場合、には必ず無害化が必要ですが、エスケープでもAPIでも無害化できないケースがあることは覚えておく必要があります。
今のPHPのファイル関連APIはヌル文字インジェクション7を防止するため、ヌル文字を含むパスを許しません。しかし、サードパーティー製のモジュールの場合、ヌル文字を含むパスを許可しているかも知れません。ヌル文字エスケープで対応することも可能ですが、こういったケースの場合はバリデーションし、エラーの場合はプログラムを停止させた方が良いです。
PHP文字列(PHP文字リテラル)
ここからが本題の、PHP文字列型データのエスケープ方法の解説です。PHPスクリプトを生成する際に、文字列リテラルを変数から生成する時に必要なエスケープになります。
エスケープ処理をしないでPHP文字列を出力すると不正なコードを実行される可能性があります。例えば、ユーザー入力を利用して設定ファイルを作成する場合にエスケープ処理をしないと脆弱になります。
※ 以下のスクリプトはシステム管理者がアプリケーション設定用のPHPファイルなどをインストールスクリプトで作る場合を想定しています。一般にユーザー入力を使ってPHP文字列(PHP文字リテラル)を生成する場合、エスケープ前の入力バリデーションが必須です。セキュアコーディングでは入力処理時の入力バリデーションと、出力処理時の出力の無害化(エスケープ/API利用/バリデーション)は独立した対策なので両方実施します。
<?php // ユーザー設定を設定ファイルに書き出す $config_filename = '../config.php'; // 入力バリデーション無しで変数を利用するのは禁止。 // ここではバリデーション済みであるとしますが、たまたまバリデーションが漏れていたとします。 $config = '<?php $admin_user = \''. $_GET['admin_user'] . '\''; file_put_contents($config_filename, $config);
$_GET[‘admin_user’]に
'; echo file_get_contents('/etc/passwd'); //
が設定されていた場合、config.phpは以下のようになります。
<?php $admin_user = ''; echo file_get_contents('/etc/passwd'); // ';
これをPHPスクリプトとして読み込むと、/etc/passwdファイルの中身が送信されてしまいます。
PHP文字列をテキストとして出力する場合、addslashes関数を利用します。addslashes関数はPHP文字列として出力する為にエスケープが必要な文字を \ (バックスラッシュ)でエスケープします。
addslashes関数がエスケープする文字
- ‘(シングルクォート)
- “(ダブルクォート)
- \(バックスラッシュ)
- NUL (ヌル文字)
PHPの任意コードが実行できる脆弱性を修正したスクリプトは以下の様になります。
<?php // ユーザー設定を設定ファイルに書き出す $config_filename = '../config.php'; // 入力バリデーション無しで変数を利用するのは禁止。 // ここではバリデーション済みであるとしますが、たまたまバリデーションが漏れていたとします。 $config = '<?php $admin_user = \''. addslashes($_GET['admin_user']) . '\''; file_put_contents($config_filename, $config);
先ほどの攻撃用文字列をaddslashes関数を利用した場合の出力は
<?php $admin_user = '\'; echo file_get_contents(\'/etc/passwd\'); // ';
となり、ユーザー入力の$_GET[‘admin_user’]がプログラムの一部として解釈されず文字列として保存されます。
addslashes関数はPHP文字列が問題なく処理される為に必要な最低限の文字をエスケープします。PHPの文字列は以下のエスケープもサポートしています。
\n | linefeed (LF or 0x0A (10) in ASCII) |
\r | carriage return (CR or 0x0D (13) in ASCII) |
\t | horizontal tab (HT or 0x09 (9) in ASCII) |
\v | vertical tab (VT or 0x0B (11) in ASCII) (since PHP 5.2.5) |
\e | escape (ESC or 0x1B (27) in ASCII) (since PHP 5.4.0) |
\f | form feed (FF or 0x0C (12) in ASCII) (since PHP 5.2.5) |
\\ | backslash |
\$ | dollar sign |
\” | double-quote |
\[0-7]{1,3} | the sequence of characters matching the regular expression is a character in octal notation |
\x[0-9A-Fa-f]{1,2} | the sequence of characters matching the regular expression is a character in hexadecimal notation |
“(ダブルクォート)で囲まれた文字列とHeredoc、では$varが現れた場合、$varは変数の内容に置換されます。PHPは ‘ (シングルクォート)で囲まれた文字列とNowdocは文字列中に$が現れても変数の開始として処理しません。 $が現れてもエスケープする必要はありません。
<?php $var = 'XYZ'; echo '$var'; // $varを出力 echo '\$var'; // \$varを出力 echo "$var"; // XYZを出力 echo "\$var"; // $varを出力
エスケープ文字でない文字をエスケープした場合、エスケープ文字も出力されます。この仕様はPythonと同じです。参考:Ruby, Perlはエスケープ文字が削除されて出力されます。
<?php echo "\a\b\c";
は
\a\b\c
を出力します。警告エラーなどは発生しません。
他のエスケープ方法
addslashes関数でエスケープする以外に、addcslashes関数でエスケープしたり、バイナリをテキストに変換するbase64_encode関数、rawurlencode関数などを利用する事もできます。
PHP文字列となる変数をaddslashes関数でエスケープした場合、デコード処理は必要ありません。他の方法でエスケープした場合はデコード処理が必要になります。
var_export関数
PHP変数をテキストとして出力するvar_export関数を利用すれば、エスケープ処理されて出力されます。addslashes関数の代わりにvar_export関数を使用したコードも安全です。
<?php // ユーザー設定を設定ファイルに書き出す $config_filename = '../config.php'; // 入力バリデーション無しで変数を利用するのは禁止。 // ここではバリデーション済みであるとしますが、たまたまバリデーションが漏れていたとします。 $config = '<?php $admin_user = '. var_export($_GET['admin_user'], true) .';'; file_put_contents($config_filename, $config);
$_GET[‘admin_user’]には配列も設定できます。
例: http://example.com/update_config.php?admin_user[]=abc&admin_user[]=xyz
var_export関数はPHP変数であれば、配列・オブジェクトをPHPスクリプトとして読み込んでも安全な形でエクスポートします。
まとめ
変数をPHP文字列として保存する場合、エスケープ処理は必須です。エスケープ処理を行わないと、任意のPHPコードを実行される可能性があります。PHP文字列を保存する際には、エスケープ処理の有無に関わらず常にaddslashes関数などでエスケープしてから保存します。
エスケープ処理を自動的に行うvar_exportは便利ですが、配列やオブジェクトも問題なく出力します。この動作は意図しない場合もあるので、データ型をチェックするなどの注意が必要です。
追記
@kenji_s さんよりSJISを使った場合は
http://localhost/test.php?admin_user=%95′;%20$a=%3C%3C%3CEOL%0D%0A/etc/passwd%0D%0AEOL;%0D%0Aecho%20file_get_contents($a);%20//
な形で攻撃可能ですね、と指摘を頂きました。
addslashes関数は非マルチバイト文字対応なので文字エンコーディングに関係なく、エスケープバイトが現れるとエスケープします。つまり、ISO-8859-1非互換のSJISなどを使っている場合にはaddslashes/var_exportは使えません。使うと脆弱になります。文字エンコーディングをバリデーションした後、SJIS対応のaddslashes関数を自作してエスケープする必要があります。(mbstring関数を使って、エスケープ対象バイトを\でエスケープ)
どの文字エンコーディングを使っていても入力時にバリデーション(文字エンコーディングが正しいかを確認)しなければなりませんが、SJISなどの場合は特に注意が必要です。PHPの文字列型はバイナリセーフなので壊れた文字エンコーディングでも何でも受け入れます。SJISなどの文字エンコーディングの場合、zend_multibyteも正しく設定(有効に設定)、スクリプトエンコーディングとmbstringの内部エンコーディングをSJISに設定してください。(入力文字エンコーディングをSJISからUTF-8に変換している場合、スクリプトエンコーディング・内部エンコーディングはUTF-8になります)
@kinji_sさん、鋭い指摘をありがとうございました。
現在のPHPでSJISを使う場合、ロケールでSJISを使うように設定する事も重要です。Windowsでは通常SJISになっていると思いますが、UNIX系OSではUTF-8になっている事が多いと思います。少なくとも5.5までのPHPはescapeshellarg/filegetcsv関数などでロケールを利用してマルチバイト文字処理を行っています。文字エンコーディングを正確に利用・設定することはセキュリティ対策には必須ですが、設定のミスマッチがあると脆弱になる場合があります。特にSJISなどの文字エンコーディングで注意が必要です。
- 2017年版OWASP TOP 10に新しく追加されたA10「不十分なログとモニタリング」に対応するため、アプリケーションでは出力でもエスケープではなくバリデーションが推奨されます。 ↩
- もしテキストの中に一文字も意味がある文字がない場合、コンテクストを考える必要はありません。それはただの”テキスト”です。反対に一文字でも意味がある文字がある場合(ヌル文字、改行文字、CSFのセパレータ文字等も!)はコンテクストがあります。 ↩
- ISO 27000/ISMSも要求している セキュアコーディングでは入力と出力は独立した処理として扱います。セキュアコーディング原則7番目の「出力の無害化」で独立した対策であると解説されています。 ↩
- 出力処理の時点では、入力ミスの値を廃除するには手遅れです。入力ミスはロジックで処理し、出力処理は”単一責任の原則”に従い、不正な値だけは最低限廃除するようにすると良いでしょう。 ↩
- 出力時のバリデーションでも不正な値を検出できます。しかし、出力時のバリデーションだとプログラミングの基本原則であるFail Fast原則に反します。 ↩
- 入力バリデーションを行ずに出力バリデーションで不正な値を検出するコードは責任の所在が滅茶苦茶なコード、所謂「スパゲッティーコード」です。入力処理は入力値の妥当性保証、ロジック処理は論理的妥当性保証、出力処理は出力先に対する無害化の責任を持ちます。出力時には何の様な入力コンテクスト(入り口)から入ってきたデータかも、論理的な妥当性も判りません。無理矢理バリデーションすると滅茶苦茶なロジックになります。これを回避する為、出力時の不十分なバリデーションで済ます(=最低限のバリデーションで済ます)ことになります。出力時のバリデーションだけでは「不確実性」を効果的に廃除できません。 ↩
- ヌル文字はC言語の文字列終端文字です。バイナリセーフなPHPと非バイナリセーフなC言語で文字列解釈が異なる為、ファイルパスにヌル文字を挿入することによって異なる解釈をさせ、不正なファイル読み取り/実行が行えます。ヌル文字インジェクション攻撃の攻撃対象はファイルパスに限りません。ヌル文字が終端文字や特別な意味と解釈されるコンテクストではヌル文字インジェクションのリスクがあります。 ↩