Security

PHPのセキュリティ対策

投稿日:2016年6月30日 更新日:

PHP を安全に使うため、気を付けるべきポイントについて書いています。

目次

1. ユーザーによる入力値の検証

  • mb_check_encoding関数で文字エンコーディングが正しいかチェックする。
  • 制御文字を入力不可としてよい場合は、正規表現等でチェックする。

2. クロスサイトスクリプティング(XSS)対策

1) HTML テキストの入力を許可しない場合の対策

  • 表示の際に文字列をHTMLエスケープすることを徹底する。
  • 信頼できない値を出力する場所(コンテキスト)によって、エスケープ方法が異なる。
置かれている場所 説明 エスケープの概要
要素内容(通常のテキスト) タグと文字参照が解釈される。「<」で終端 「<」と「&」を文字参照に
属性値 文字参照が解釈される。引用符で終端 属性値を「”」で囲み、「<」と「”」と「&」を文字参照に
属性値(URL) 同上 URLの形式を検査してから属性値としてのエスケープ
イベントハンドラ 同上 JavaScriptとしてエスケープしてから属性値としてのエスケープ
script要素内の文字列リテラル タグも文字参照も解釈されない。「</」により終端 JavaScriptとしてのエスケープおよび「</」が出現しないよう考慮

引用元:体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践

要素内容(通常のテキスト)

このコンテキストの例

<div> ...ここに出力したい... </div>
エスケープ方法
htmlspecialchars($str, ENT_QUOTES, $charset)
  • 第三引数をちゃんと指定すること。但し、PHP 5.6.0 以降では、デフォルト値として default_charset の値が使用される。
  • 参考:PHP: htmlspecialchars – Manual

属性値

このコンテキストの例

<input id="foo" name="foo" value="...ここに出力したい... "/>
  • 文字参照が解釈されることに注意する。
エスケープ方法
  • 必ずダブルクォートで囲む。
  • その上で以下のようにエスケープする。
htmlspecialchars($str, ENT_QUOTES, $charset)

属性値(URL)

href や src 属性に指定する値

このコンテキストの例

<a href="...ここに出力したい... "/>foo</a>
<iframe src="...ここに出力したい... "/>
  • エスケープ処理と共に、ドメインのチェックも必要。
  • 文字参照が解釈されることに注意する。
エスケープ方法
  • URL Scheme をチェックする。
    • 例えば、”http” or “https” or “/” が先頭にあればOKとする。
  • URLの各パラメータ値に信用できない値を指定する場合は、この値の部分を URLエンコードする。
    $param1 = rawurlencode($_POST['param1']);
    $url = "http://example.com/?param1=" . $params1;
  • 通常は rawurlencode() を使用する。
  • URL全体を以下のようにHTMLエスケープする。
    $param1 = rawurlencode($_POST['param1']);
    $param2 = rawurlencode($_POST['param2']);
    $url = "http://example.com/?param1=" . $params1 . "&param2=" . $param2;
    $url_escaped = htmlspecialchars($url, ENT_QUOTES, $charset);
    // この $url_escaped を href や src の値としてセットする。
  • 属性値(URL)のエスケープで文字列はどう変化するか?の例を以下に示す。
    // 以下のURLを aタグのsrc属性値に指定したい場合(ageパラメータ値はユーザーが指定したとする)
    $url = "http://www.example.com/?name=taro&age=<script>alert(1)";
    //
    // まずユーザが入力したパラメータ値をrawurlencodeする。
    //
    $url1 = "http://www.example.com/?name=taro&age=" . rawurlencode("<script>alert(1)");
    //
    // 次のように、パーセントエンコーディングされる。
    //   ↓
    // http://example.com/?name=taro&age=%3Cscript%3Ealert%281%29%3C%2Fscript%3E
    // 次にURL全体をHTMLエスケープする
    //
    $url2 = htmlspecialchars($url1, ENT_QUOTES, 'UTF-8');
    //
    // すると、&が文字参照の&amp;になる。これを aタグのsrc属性値として指定すれば良い。
    //   ↓
    // http://example.com/?name=taro&amp;age=%3Cscript%3Ealert%281%29%3C%2Fscript%3E

イベントハンドラ属性値

このコンテキストの例

<div onmouseover="func('...ここに出力したい...')">検知したら実行させない</div>
  • 信頼できない値を出力するのは、クォートで囲まれた文字列リテラル内に限定する。それ以外は危険。
  • 文字参照が解釈されることに注意する。
エスケープ方法
  1. JavaScriptの文字列リテラルにおいて、エスケープシーケンスでの表現が必要な文字はそれに従った記述を行う。
  2. HTMLエスケープする。
  3. 属性値としてダブルクォートで囲む
エスケープ方法(Unicodeエスケープを使う方法)
  • 文字列リテラルに出力する値をエスケープする1つの方法は、英数文字以外を Unicodeエスケープしてしまうことである。
    • この方法であれば、HTMLタグの文字列があったとしても、HTMLとして解釈されることはなく、あくまでJavaScriptの中でエスケープ前の文字列として使用されるだけになる。つまりこれだけやれば文字列を埋め込める。
JavaScriptの文字列リテラルを生成する関数の例
  • 体系的に学ぶ 安全なWebアプリケーションの作り方 脆弱性が生まれる原理と対策の実践 に掲載されていたスクリプトを少し変更してある。詳細はこちらの書籍を参考にして欲しい。
  • JavaScriptのエスケープ方法が複雑なため、1つ1つきめ細かいエスケープをするのではなく、数値とアルファベット(と「-」、「.」も)以外はざっくりとUnicodeエスケープしている。
    function escape_js_string($s) {
      return preg_replace_callback('/[^-\.0-9a-zA-Z]+/u', function($matches){
          $u16 = mb_convert_encoding($matches[0], 'UTF-16');
          return preg_replace('/[0-9a-f]{4}/', '\u$0', bin2hex($u16));  
      }, $s);
    }

scriptタグ内

このコンテキストの例

<script>var foo="...ここに出力したい... ";</script>
  • 信頼できない値を出力するのは、クォートで囲まれた文字列リテラル内に限定する。それ以外は危険。
エスケープ方法
  1. JavaScriptの文字列リテラルにおいて、エスケープシーケンスでの表現が必要な文字はそれに従った記述を行う。
  2. HTMLエスケープする。
  3. </script がある場合は、<\/script に変換する。
参考
エスケープ方法(Unicodeエスケープを使う方法)
  • 文字列リテラルに出力する値をエスケープする1つの方法は、英数文字以外を Unicodeエスケープしてしまうことである。
    • この方法であれば、</ もエスケープされるのでスクリプトが終端される心配はないし、HTMLエスケープもする必要がない。
    • 但し、この方法でエスケープした文字列を setAttributeメソッド等で、イベントハンドラ属性に指定すると JavaScriptコードとして実行されてしまうので注意する。(参考: DOM based XSS Prevention Cheat Sheet – OWASP
JavaScriptの文字列リテラルを生成する関数の例
  • 「イベントハンドラ属性値のエスケープ」に書いたものと同様。

JavaScriptに値を渡す方法

  • JavaScriptの文字列リテラルに値を直接埋め込むのではなく、間接的に値を渡す方が安全である。
データセット属性を使う方法
  • HTMLタグの属性を data-xxx="{{ 信用できない値 }}" というように生成しておき、JavaScriptから取得させる。PHP側で値をセットする
    <div id="foo" data-bar="<?php echo htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); ?>"/>

    JavaScript側で値を取り出す

    var value = document.querySelector('#foo').dataset.bar;
    // もしくは
    var value = document.querySelector('#foo').getAttribute('bar');
hiddenパラメータを使う方法
  • inputタグのtype属性にhiddenを指定して値をセットし、JavaScriptから取得させる。PHP側で値をセットする
    <input type="hidden" id="foo" value="<?php echo htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); ?>"/>

    JavaScript側で値を取り出す

    var value = document.querySelector('#foo').value;

参考

2) HTML テキストの入力を許可する場合の対策

3) 全てのウェブアプリケーションに共通の対策

  • 文字コードの指定
    1. php.ini 内で default_charset を指定する。(これにより、HTTP レスポンスヘッダの Content-Type フィールドに文字コードがセットされる)php.ini
      default_charset = "UTF-8"
    2. meta要素を記述する。HTMLファイルのヘッダ部分
      <meta charset="UTF-8">
  • HTMLタグの属性値はダブルクォートで囲む
  • セッションクッキーに HttpOnly属性を設定する。
    ini_set('session.cookie_httponly', 1);
  • HTTPレスポンスヘッダに「X-XSS-Protection」を設定して XSS攻撃を検知させる。
// 検知したら実行させない
  header("X-XSS-Protection: 1; mode=block");
  • HTTPレスポンスヘッダに「Content-Security-Policy」を設定する。
// JavaScriptの実行を許可する対象を 同一オリジンと code.jquery.com と maxcdn.bootstrapcdn.com に制限する
  header("Content-Security-Policy: default-src 'self'; script-src 'self' code.jquery.com maxcdn.bootstrapcdn.com");

参考

3. SQLインジェクション対策

  • エンコーディングの指定
  • プレースホルダの利用
  • データベースに接続するユーザの権限を限定する。

PDOを使う場合

  • DBへの接続と設定
    $options = array(
      // 静的プレースホルダを指定
      PDO::ATTR_EMULATE_PREPARES => false,
      // エラー発生時に例外を投げる
      PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
    );
    // 複文を禁止する (この定数は、PHPのバージョン 5.5.21 および PHP 5.6.5 以上で使用できる)
    if (defined('PDO::MYSQL_ATTR_MULTI_STATEMENTS')) {
      $options[PDO::MYSQL_ATTR_MULTI_STATEMENTS] = false;
    }
    
    try {
      // charset は set names でセットせず、この第1引数で指定すること(PHP5.3.6以降で可能)
      $db = new PDO('mysql:host=localhost;dbname=foo;charset=utf8',
            'username', 'password', $options);
    } catch (PDOException $e) {
      // $e->getMessage() などでエラー情報を取得できる
    }
  • 名前付けされたプレースホルダを用いてプリペアドステートメントを実行するコード例 (PHP: PDOStatement::bindValue – Manualから抜粋)
    $calories = 150;
    $colour = 'red';
    $sth = $dbh->prepare('SELECT name, colour, calories
        FROM fruit
        WHERE calories < :calories AND colour = :colour');
    $sth->bindValue(':calories', $calories, PDO::PARAM_INT);
    $sth->bindValue(':colour', $colour, PDO::PARAM_STR);
    $sth->execute();
    
    • ここでは、$calories$colour に直接値が代入されていますが、ユーザーの入力した値が代入された場合の危険性を考えて下さい。
    • bindValue() メソッドの第三引数に、PDO::PARAM_* 定数で型を指定します。
  • 疑問符プレースホルダを用いてプリペアドステートメントを実行するコード例 (PHP: PDOStatement::bindValue – Manualから抜粋)
    $calories = 150;
    $colour = 'red';
    $sth = $dbh->prepare('SELECT name, colour, calories
        FROM fruit
        WHERE calories < ? AND colour = ?');
    $sth->bindValue(1, $calories, PDO::PARAM_INT);
    $sth->bindValue(2, $colour, PDO::PARAM_STR);
    $sth->execute();
    
    • ここでは、$calories$colour に直接値が代入されていますが、ユーザーの入力した値が代入された場合の危険性を考えて下さい。
    • bindValue() メソッドの第三引数に、PDO::PARAM_* 定数で型を指定します。

参考

(仕方なく)生のSQL文を書く場合の例

  • 信用できない値を埋め込む場合は、以下を参考にしてちゃんとエスケープ処理する。

エスケープ方法

  • 整数リテラルには intval() 関数を通す。
    • PDO::quote メソッドの第2引数に PDO::PARAM_INT を指定してエスケープすればよさそうだが、これには実は問題があるため使わない。(「参考」のリンク先を参照)
  • 文字列リテラルは PDO::quote メソッドを使って引用符で囲む(引用符自体があればエスケープされる)。
    • 他のデータベース抽象化ライブラリにも大抵は似たようなメソッドがある。
    • データベース毎に用意されたエスケープ用関数(例えば MySQLなら mysqli_real_escape_string 関数)でもよい。

    $item_id = intval($_POST["item_id"]);
    $name    = $db->quote($_POST["name"], PDO::PARAM_STR);
    $result  = $dbh->exec(
      "INSERT INTO items (item_id, name)".
      "VALUES($item_id, $name)");

参考

4. クロスサイトリクエストフォージェリ(CSRF)対策

  • トークンを埋め込んで接続元を判定する。
  • トークンの生成には、暗号論的擬似乱数生成器を使用する。
    • PHP 7.0移行であれば、random_bytes 関数を使うと良い。
      • 例: $csrf_token = base64_encode(random_bytes(64));
    • PHP5.3.0以降であれば、openssl_random_pseudo_bytes 関数を使うとよい。
    • PHP 5.x であれば、random_bytes 関数の代わりとして random_compat を使うと良い。
  • ワンタイムトークンを使う必要はない。

5. OSコマンド・インジェクション対策

  • なるべく、外部からのパラメータ値をOSコマンド(パラメータを含む)に使用しない。
  • 外部からは番号等を指定させ、実際にコマンドに使用する文字列は予め用意された文字列を使用する。
  • パラメータのエスケープには、escapeshellarg 関数を利用する。

6. ディレクトリ・トラバーサル対策

  • できれば、ファイル名を外部から指定させない。
  • basename() 関数を利用する。
    • 但し、basename 関数はヌルバイトを削除しないので自分で削除する必要がある。もしくは、逆にホワイトリスト方式で許可する文字だけで構成されているかチェックする。
    • また basename() は setlocale()がちゃんと設定されている必要がある。

    注意:
    basename() はロケールに依存します。 マルチバイト文字を含むパスで正しい結果を得るには、それと一致するロケールを setlocale() で設定しておかなければなりません。

    引用元:PHP: basename – Manual

7. 安全でないデシリアライゼーション (Insecure Deserialization) 対策

  • unserialize() 関数に、外部からの値を渡さない。
  • シリアル化したデータをユーザーに渡す必要がある場合は、安全で標準的なデータ交換フォーマットである JSON などを使う。(json_decode() および json_encode() を利用する)

参考

8. XML外部実体参照(XXE) 対策

  • libxml2 を最新にする。

参考

9. メールヘッダインジェクション対策

  • できれば、外部からのパラメータ値をメールヘッダに埋め込まないようにする。
  • 外部から指定するメールアドレスをバリデーションする。
    • メールアドレスのバリデーション:PHP: 検証 – Manual
    • 件名のバリデーション:「制御文字以外にマッチする」正規表現を利用する。

参考

10. HTTPヘッダ・インジェクション対策

  • header関数を使えばよさそう。
  • 但し、リダイレクトさせる場合はドメインをチェックする。

メモ

11. オープン・リダイレクト対策

リダイレクトには以下の3パターンがある。

  1. レスポンスヘッダ(“Location: URL”)でリダイレクトさせる
    header('Location: http://www.example.com/');
  2. <meta refresh>を使ってリダイレクトさせる
    <meta http-equiv="Refresh" content=0;URL=http://www.example.com/'">
  3. JavaScriptによるlocationオブジェクトへの代入によってリダイレクトさせる
    location.href = url_from_input;

対策

  • リダイレクト先のURL文字列に、ユーザーの入力した値を直接使用しない作りにする。ユーザーの入力した値を基に、アプリケーション側で用意した文字列を選択して使用する。
  • それができない場合
    • URL Scheme をチェックする(http もしくは https のみ許可するなど)
    • 許可されたドメインかどうかチェックする。
  • <meta refresh> は危険なので使わないほうが良い。

参考

12. セッション管理の不備への対策

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

  • ログイン時に、session_regenerate_id() 関数を実行してセッションIDを再生成する。
  • ログアウト後に、session_destroy() 関数を実行してセッションに登録されたデータを全て破棄する。

13. ファイルアップロード

  • アップロードされたファイルを公開ディレクトリに置かない。
  • 画像を扱う場合、BMP形式はプログラムで扱い辛い面があるため対象外にしておくのが妥当である。
  • IE7以前での画像XSS対策

画像ファイルの判定について

  • FileInfo の関数や、getimagesize関数で判定する。これらはマジックバイトでの判定であり、多少信頼性が低いので imagecreatefromstring 関数でイメージリソースが生成できるか確認しておくとよい。
  • exif_imagetype関数でも画像の種類は判定はできるが、exif 拡張モジュールを必要とする。

渡されたファイル(ファイルパス)が画像ファイルであるかどうかをチェックする関数の例

  • set_error_handler関数を使って、全てのエラーで ErrorExceptionクラスをスローしている環境を想定している。
  • imagecreatefromstring関数は環境によって対応している画像フォーマットが違ってくるらしいので、対応している画像フォーマットのみに使用する。
/**
 * @param String $filepath ファイルパス(拡張子は当てにしない)
 * @return bool
 */
function isValidImageFile($filepath)
{
    try {
        // WARNING, NOTICE が発生する可能性あり
        $img_info = getimagesize($filepath);

        switch ($img_info[2]) {
            case IMAGETYPE_GIF:
            case IMAGETYPE_JPEG:
            case IMAGETYPE_PNG:
                // イメージリソースが生成できるかどうかでファイルの中身を判定する。
                // データに問題がある場合、WARNING が発生する可能性あり
                if (imagecreatefromstring(file_get_contents($filepath)) !== false) {
                    return true;
                }
        }

    } catch (\ErrorException $e) {

        // ログ出力する文字列の例
        $err_msg = sprintf("%s(%d): %s (%d) filepath = %s",
            __METHOD__, $e->getLine(), $e->getMessage(), $e->getCode(), $filepath);

        // TODO:
        //   - $e->getSeverity() の値によって、ログ出力を変えたりする。

    }

    return false;
}

メモ

  • JPEG, TIFF の場合は、EXIF情報を削除することも検討する。

参考

14. パスワードの保存方法

  • PHP5.5以降が使える環境では password_hash関数を使う。(パスワードが保存されたハッシュ値にマッチするかどうか調べるには、 password_verify() を使う)

password_hash() 関数を使ったコードサンプル

POSTメソッドで送信されたパスワードをハッシュ化してセッション変数にセットする。

$_SESSION['password_hash'] = password_hash($_POST['password'], PASSWORD_DEFAULT);

POSTメソッドで送信されたパスワードと、セッション変数にセットされたハッシュ値とを照合する。

if (password_verify($_POST['password'], $_SESSION['password_hash'])) {
    echo '正しいパスワードです!';
} else {
    echo 'パスワードが間違っています';
}

15. アクセス制御や認可制御の欠落

  • 権限情報はセッション変数に保持して、権限が必要な処理の直前で必要な権限をチェックする。

16. PHPの設定値

開発環境用の設定

display_errors = On
display_startup_errors = On
error_reporting = -1
log_errors = On

全てのエラーを表示する設定(PHPのバージョン毎)

< 5.3 -1 or E_ALL
  5.3 -1 or E_ALL | E_STRICT
< 5.3 -1 or E_ALL

本番環境用の設定

display_errors = Off
display_startup_errors = Off
error_reporting = E_ALL
log_errors = On

17. WordPress

1. WordPress の各種エスケープ関数

esc_html()

esc_attr()

esc_js()

sanitize_text_field()

など…

参考

2. WordPress のデータベース操作関数

データベースの操作を行う場合、基本的には以下の関数を使用する。

get_post()

wp_insert_post()

など…

3. $wpdb オブジェクトを使って直接データベースを操作する場合

$wpdb オブジェクトを使って直接データベースを操作する場合は、wpdb::prepare() メソッドを利用してエスケープする。

参考

18. その他のメモ

  • ereg()関数はバイナリセーフでない。このためPHP 5.3.0 で非推奨となった。
  • アプリケーション開発者は Composer を使うことで、require / require_once / include / include_once に起因するファイルインクルード攻撃についてはあまり心配しなくてよくなった(これらを直接使うことはなくなったので)。
  • eval() 関数は使わない。
  • 本記事では、IE7以前に実装されていた「CSS Expressions」機能については触れていない。
  • 「外部からコントロールできる値をunserialize関数に処理させない」

19. 参考

関連

こちらの記事もご覧ください。

Web Security

JavaScript とHTML5のセキュリティ対策

2016.06.30
PHP

Laravel 5 でのセキュリティ対策 (PHP)

2016.06.30
Web Programming

Webプログラミングのためのリンク集

2017.06.20

スポンサードリンク

📂-Security
-

執筆者:labo


comment

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

関連記事

Web Security

「秘密の質問」機能について

目次「秘密の質問」とは?「秘密の質問」をどう使えばよいのか?参考サイト 「秘密の質問」とは? 「秘密の質問」とは、「質問」とそれに対応する本人しか知らない「答え」を設定し、パスワードリマインダやインタ …

no image

HTTP ヘッダ・インジェクション by IPA「安全なウェブサイトの作り方 第7版」

安全なウェブサイトの作り方:IPA 独立行政法人 情報処理推進機構の、「HTTP ヘッダ・インジェクション」から一部抜粋する。(この資料はPDFでしか提供されていない) 目次注意が必要なウェブ …

no image

ウェブサイトの安全性向上のための取り組み – パスワードに関する対策 by IPA「安全なウェブサイトの作り方 第7版」

ウェブサイトの安全性向上のための取り組み – パスワードに関する対策 by IPA「安全なウェブサイトの作り方 第7版」 パスワードに関する対策 初期パスワードは、推測が困難な文字列で発行 …

no image

Webサイト/スマートフォンに関するセキュリティインシデント情報

目次Webサイトに関するセキュリティインシデント情報のリンクスマートフォンに関するセキュリティインシデント情報のリンクその他 Webサイトに関するセキュリティインシデント情報のリンク 2016-11- …

no image

クリックジャッキング by IPA「安全なウェブサイトの作り方 第7版」

安全なウェブサイトの作り方:IPA 独立行政法人 情報処理推進機構の、「クリックジャッキング」から一部抜粋する。(この資料はPDFでしか提供されていない) 目次注意が必要なウェブサイトの特徴根本的解決 …