PHPでの最低限のセキュリティ対策のやり方(前編)
2016-01-21
PHPにおける基本的なセキュリティ対策の方法について、備忘録としてまとめます。
PHPでこれからWebアプリケーションやWebサイトを作成しようと考えている方は、各項目について対策は万全か、是非一度ご確認ください。
本記事(前編)では、以下に関する脆弱性について取り扱います。
- SQLインジェクション
- OSコマンドインジェクション
- ディレクトリトラバーサル
- セッション管理の不備
- クロスサイトスクリプティング(XSS)
以下の脆弱性については、後編の記事をご覧ください。
- クロスサイトリクエストフォージェリ(CSRF)
- HTTPヘッダインジェクション
- メールヘッダインジェクション
- クリックジャッキング
参考サイト
安全なウェブサイトの作り方:IPA 独立行政法人 情報処理推進機構
SQLインジェクション
ユーザーによるSQLの不正実行が可能な状態となっている、「SQLインジェクションの脆弱性」を淘汰するために、PHPで必要とされる処置は以下のとおりです。
プレースホルダーやプリペアドステートメントの利用(根本的解決)
MySQLi(MySQL改良版拡張モジュール)における、mysqli::prepareメソッドを利用して、SQLを実行します。
<?php
$mysqli = new mysqli("localhost", "my_user", "my_password", "world");
/* 接続状況をチェックします */
if (mysqli_connect_errno()) {
printf("Connect failed: %s\n", mysqli_connect_error());
exit();
}
$city = "Amersfoort";
/* プリペアドステートメントを作成します */
if ($stmt = $mysqli->prepare("SELECT District FROM City WHERE Name=?")) {
/* マーカにパラメータをバインドします */
$stmt->bind_param("s", $city);
/* クエリを実行します */
$stmt->execute();
/* 結果変数をバインドします */
$stmt->bind_result($district);
/* 値を取得します */
$stmt->fetch();
printf("%s is in district %s\n", $city, $district);
/* ステートメントを閉じます */
$stmt->close();
}
/* 接続を閉じます */
$mysqli->close();
SQL文字列のエスケープ処理を実施(根本的解決)
MySQLi(MySQL改良版拡張モジュール)における、mysqli::real_escape_stringメソッドを利用して、SQLを実行します。
<?php
$mysqli = new mysqli("localhost", "my_user", "my_password", "world");
/* 接続状況をチェックします */
if (mysqli_connect_errno()) {
printf("Connect failed: %s\n", mysqli_connect_error());
exit();
}
$mysqli->query("CREATE TEMPORARY TABLE myCity LIKE City");
$city = "'s Hertogenbosch";
/* このクエリは失敗します。なぜなら $city をエスケープしていないからです */
if (!$mysqli->query("INSERT into myCity (Name) VALUES ('$city')")) {
printf("Error: %s\n", $mysqli->sqlstate);
}
$city = $mysqli->real_escape_string($city);
/* $city をエスケープしたので、このクエリは正しく動作します */
if ($mysqli->query("INSERT into myCity (Name) VALUES ('$city')")) {
printf("%d Row inserted.\n", $mysqli->affected_rows);
}
$mysqli->close();
エラーメッセージを出力しない(保険的対策)
PHPの設定でエラーや警告内容を画面上に表示されないようにします。
php.iniで設定する場合は、php.ini内に下記の記述をします。
display_errors = Off
PHPのソースコード内で設定する場合は、下記の処理をソースコードに追加します。
<?php
ini_set('display_errors', 0);
OSコマンドインジェクション
ユーザーによる意図しないOSコマンドの実行が可能な状態となっている、「OSコマンドインジェクションの脆弱性」を淘汰するために、PHPで必要とされる処置は以下のとおりです。
exec関数等のOSコマンドを実行できる関数を使用しない(根本的解決)
OSコマンドを実行できる、以下の関数を使用しないようにします。
exec関数等のOSコマンドを実行できる関数を使用する場合は、OSコマンド文字列をエスケープする(保険的対策)
escapeshellcmd関数もしくはescapeshellarg関数を使用して、OSコマンド文字列をエスケープします。
<?php
// 意図的に、任意の数の引数を指定できるようにしています
$command = './configure '.$_POST['configure_options'];
$escaped_command = escapeshellcmd($command);
system($escaped_command);
<?php
system('ls '.escapeshellarg($dir));
ディレクトリトラバーサル
ユーザーによる意図しないディレクトリへのファイル参照が可能な状態となっている、「ディレクトリトラバーサルの脆弱性」を淘汰するために、PHPで必要とされる処置は以下のとおりです。
外部からのパラメーターでファイルパスを直接指定する実装を避ける(根本的解決)
そもそも、参照するファイルを以下のように変数にしないことで、回避できます。
<?php
echo file_get_contents($_POST['file_path']);
外部からのパラメーターでファイル名のみを指定する実装にする(根本的解決)
basename関数を使用し、外部からのパラメーターにディレクトリ情報が含まれないようにすることで、回避できます。
<?php
echo file_get_contents('/tmp/example/'.basename($_POST['file_name']));
ファイル名のチェックを行う(保険的対策)
正規表現などで、パスにディレクトリトラバーサルの要因が含まれないように考慮します。
<?php
if (preg_match('/(\.\.\/|\/|\.\.\\\\)/', $_POST['file_path'])) {
echo "Error!!";
} else {
echo file_get_contents($_POST['file_path']);
}
セッション管理の不備
セッションハイジャックを回避するために、PHPで必要とされる処置は以下のとおりです。
セッションIDを推測が困難なものにする(根本的解決)
推測困難なセッションIDを生成するよう設定します。
php.iniで設定する場合は、php.ini内に下記の記述をします。
session.entropy_file = /dev/urandom
session.entropy_length = 32
PHPのソースコード内で設定する場合は、下記の処理をソースコードに追加します。
<?php
ini_set('session.entropy_file', '/dev/urandom');
ini_set('session.entropy_length', '32');
セッションIDをURLパラメーターに格納しない(根本的解決)
セッションIDをURLパラメーターに格納しない設定をします。
php.iniで設定する場合は、php.ini内に下記の記述をします。
session.use_cookies = 1
session.use_only_cookies = 1
PHPのソースコード内で設定する場合は、下記の処理をソースコードに追加します。
<?php
ini_set('session.use_cookies', '1');
ini_set('session.use_only_cookies', '1');
HTTPS通信で利用するCookieにはsecure属性を加える(根本的解決)
HTTPS通信で利用するCookieにはsecure属性を加える設定をします。
php.iniで設定する場合は、php.ini内に下記の記述をします。
session.cookie_secure = 1
PHPのソースコード内で設定する場合は、下記の処理をソースコードに追加します。
<?php
ini_set('session.cookie_secure', '1');
もしくは、setcookie関数でクッキーを送信する際に、以下のように第6引数にtrueをセットします。
<?php
setcookie("sid", "secret", 0, "", "", true);
ログイン後にセッションIDを再生成する(根本的解決)
ログイン処理の後にsession_regenerate_id関数を実行し、セッションIDを再生成します。
<?php
session_start();
$old_sessionid = session_id();
// ログイン処理
session_regenerate_id(true);
$new_sessionid = session_id();
echo "古いセッション: $old_sessionid<br>";
echo "新しいセッション: $new_sessionid<br>";
クロスサイトスクリプティング(XSS)
ユーザーの入力による意図しないスクリプトの実行が可能な状態となっている、「クロスサイトスクリプティングの脆弱性」を淘汰するために、PHPで必要とされる処置は以下のとおりです。
(HTMLテキストの入力を許可しない場合)ウェブページに出力するテキストに対して、エスケープ処理を実施する(根本的解決)
htmlspecialchars関数もしくはhtmlentities関数を用いて、出力される要素に対してエスケープ処理を実施します。
なお、どちらの関数もデフォルトではシングルクォート'
がエスケープされず、脆弱性が含まれてしまう可能性が出てきてしまうので、第二引数にENT_QUOTES
、第三引数に文字コードをセットしましょう。
<?php
echo htmlspecialchars($str, ENT_QUOTES, 'UTF-8');
(HTMLテキストの入力を許可する場合)入力されたHTMLを解析し、スクリプトを含まない必要な要素のみを抽出する(根本的解決)
DOMを操作できるクラス群などを利用して入力されたHTMLテキストを解析し、script要素やstyle要素を取り除き、必要とされる要素のみを残すような処理を実施します。
<?php
$dom = new DOMDocument();
libxml_use_internal_errors(true);
@$dom->loadHTML($_POST['input_html']);
libxml_clear_errors();
while (($r = $dom->getElementsByTagName("script")) && $r->length) {
$r->item(0)->parentNode->removeChild($r->item(0));
}
echo $dom->saveHTML();
正規表現などで、入力値のチェックを行う(保険的対策)
preg_match関数やpreg_replace関数などを用いて、スクリプトが含まれない入力値であるかチェックします。
HTTPレスポンスヘッダのContent-Typeフィールドに文字コード(charset)の指定を行う(根本的解決)
HTTPレスポンスヘッダのContent-Typeフィールドに文字コード(charset)の指定を行う記述を、HTMLのドキュメント出力前に実行します。
<?php
header("Content-Type: text/html; charset=UTF-8");
発行するCookieにHttpOnly属性を加える(保険的対策)
発行するCookieにHttpOnly属性を加える設定をします。
php.iniで設定する場合は、php.ini内に下記の記述をします。
session.cookie_httponly = 1
PHPのソースコード内で設定する場合は、下記の処理をソースコードに追加します。
<?php
ini_set('session.cookie_httponly', '1');
WebブラウザのXSSフィルター機能を有効にするレスポンスヘッダを返す(保険的対策)
HTTPレスポンスヘッダのX-XSS-Protectionフィールド出力の記述を、HTMLのドキュメント出力前に実行します。
<?php
header("X-XSS-Protection: 1; mode=block");
まとめ
本記事では、
- SQLインジェクション
- OSコマンドインジェクション
- ディレクトリトラバーサル
- セッション管理の不備
- クロスサイトスクリプティング(XSS)
について、PHPにおける具体的なセキュリティ対策を述べていきました。
よろしければ、後編の記事もご覧ください。