Web Application Security Memo

ウェブセキュリティに関するメモ書き

PHPのセキュリティ対策

※当サイトにはプロモーションが含まれています。

公開日: 更新日:

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

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

クロスサイトスクリプティング(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 = urlencode($_POST['param1']);
    $url = "http://example.com/?param1=" . $params1;
  • URL全体を以下のようにHTMLエスケープする。

    $param1 = urlencode($_POST['param1']);
    $param2 = urlencode($_POST['param2']);
    $url = "http://example.com/?param1=" . $params1 . "&" . $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)";
    //
    // まずユーザが入力したパラメータ値をurlencodeする。
    //
    $url1 = "http://www.example.com/?name=taro&age=" . urlencode("<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');
    //
    // すると、&が文字参照の&になる。これを aタグのsrc属性値として指定すれば良い。
    //   ↓
    // http://example.com/?name=taro&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. &lt;/script がある場合は、&lt;\/script に変換する。
参考
エスケープ方法(Unicodeエスケープを使う方法)
  • 文字列リテラルに出力する値をエスケープする1つの方法は、英数文字以外を Unicodeエスケープしてしまうことである。
    • この方法であれば、&lt;/ もエスケープされるのでスクリプトが終端される心配はないし、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"
      • PHP 5.6.0 以降は “UTF-8” がデフォルトになっている。
      • すべてのバージョンの PHP は、PHP から送信する Content-Type ヘッダのデフォルト値としてこの設定値を使う。(ただし、header() で上書きは可能)
      • 参考:PHP: コア php.ini ディレクティブに関する説明 - Manual
    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");

参考

SQLインジェクション対策

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

PDOを使う場合

(仕方なく)生の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)");

参考

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

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

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

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

  • できれば、ファイル名を外部から指定させない。

  • basename関数を利用する。

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

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

    引用元:PHP: basename - Manual

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

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

参考

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

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

メモ

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

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

  1. レスポンスヘッダ(“Location: URL”)でリダイレクトさせる

    header('Location: http://www.example.com/');
  2. を使ってリダイレクトさせる

    <meta http-equiv="Refresh" content=0;URL=http://www.example.com/'">
  3. JavaScriptによるlocationオブジェクトへの代入によってリダイレクトさせる

    location.href = url_from_input;

対策

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

参考

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

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

  • ログイン時に、session_regenerate_id() を実行してセッションIDを再生成する。

ファイルアップロード

  • アップロードされたファイルを公開ディレクトリに置かない。
  • 画像を扱う場合、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情報を削除することも検討する。

参考

パスワードの保存方法

  • PHP5.5以降が使える環境では password_hash関数を使う。

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

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

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

その他のメモ

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

参考

広告