クロスサイトスクリプティング対策 ホンキのキホン

本稿はCodeZineに2015年12月28日に掲載された記事の再掲となります。
クロスサイトスクリプティング(XSS)は、古くから存在し開発者にもっともよく知られたセキュリティ上の問題のひとつでありながら、OWASP Top 10でも2010年に引き続き2013年でも3位と、未だに根絶できていない脆弱性です。
本記事では、Webアプリケーションの開発においてXSSを根絶するために必要な対策の基本を本気でお伝えします。

はじめに

OWASPでは開発者に向けたセキュリティ対策のためのドキュメントやチートシートを多数用意しており、XSSへの対策としても「XSS (Cross Site Scripting) Prevention Cheat Sheet」というドキュメントが用意されています。
ただし、このXSS Prevention Cheat Sheetはシンプルなルールを定めたチートシートであると謳いつつも、開発者が直面するあらゆる場面で包括的に適用できる抜けのないものを目指しているために、多岐に渡る条件のもとでの詳細なルールが定められており、残念ながら決して誰しもが簡単にすぐ適用できるものではないというのが現状です。
そこで、本記事ではより一般的で頻繁に遭遇する共通的な状況下に限定することで、XSSへの対策方法を簡潔に説明したいと思います。
本記事では物足りない方、本記事だけでは自身の開発しているWebアプリケーションへの対策として不十分だと感じた方はぜひXSS Prevention Cheat Sheetのほうにも目を通してみてください。

復習: そもそもXSSとは

XSSへの対策方法を解説する前に、改めてXSSとはどのような脆弱性であるのかを振り返っておきましょう。
例えば、http://shop.example.jp/search?item=OWASP というURLでアクセスすると、商品名に「OWASP」を含む商品の一覧を表示するショッピングサイトがあったとします。

レスポンスとして返されるHTMLの一部には、URLのitemパラメータで指定された文字列「OWASP」が含まれています。

<div>
    <span>OWASP</span>に関する検索結果:20件
</div>

ここで、http://shop.example.jp/search?item=<s>OWASP</s>のように「OWASP」という文字列のかわりに「<s>OWASP</s>」という文字列を与えてアクセスした場合に、サーバ側がそのまま「<s>OWASP</s>」という文字列をHTML内に含めて返したとします。

<div>
    <span><s>OWASP</s></span>に関する検索結果:0件
</div>

ブラウザ上に文字列として「<s>OWASP</s>」と表示させるには、HTML内には「&lt;s&gt;OWASP&lt;/s&gt;」と生成されなければいけないのですが、サーバ上でHTMLを生成する際に「<」や「>」をエスケープせずにそのまま出力しているために、ブラウザ内ではそのままHTMLタグとして取り扱われてしまいます。
このように、HTMLを生成する段階でのエスケープに漏れのある状態で、攻撃者が「<script>alert("hacked!")</script>」のような文字列を与え、そのときのURL http://shop.example.jp/search?item=<script>alert("hacked!")</script> *1SNSなどを通じて多数の人に拡散したとします。
このURLにアクセスした一般の利用者は、ブラウザ上でいきなり表示される「hacked!」のメッセージに、このショッピングサイトが攻撃者に改ざんされてしまったのではないかと強い不安を感じてしまうでしょう。

この例では攻撃者はalert関数によってメッセージを表示させるという単純で実質的には無害なことしか行っていませんが、攻撃者の作成したJavaScriptが利用者のブラウザ上で動くということは、より悪質な攻撃が行えるということを意味しています。
例えば、攻撃者はJavaScriptによってHTMLに自由にメッセージを書き込む見かけ上の改ざんを行うこともできますし、このショッピングサイトに利用者自身が登録している氏名や住所、メールアドレスなどを盗み見ることもできます。また、偽のクレジットカード番号入力画面を作成し、そこに入力されたカード番号を搾取することや、セッションCookieにhttponly属性が付与されていない場合にはdocument.cookieを通じて利用者のセッションを攻撃者がまるまる乗っ取ってしまうこともできます。
このように、HTMLを生成する際に文字列がエスケープされておらず、攻撃者によって作成されたHTMLタグやJavaScriptが利用者のブラウザ上で表示、実行される脆弱性XSSと呼びます。

XSSの発生する原因

先ほどの例では、テキストとして表示されるべき部分(テキストノード)に攻撃者によって与えられた「<」や「>」がエスケープされずそのまま埋め込まれたためにHTMLのタグとして扱われることでXSSが発生しましたが、それ以外にもXSSが発生する原因としてよくあるのは、属性値へ出力する際に「"」や「'」がエスケープされていないというものです。
前述と同じく http://shop.example.jp/search?item=OWASP というURLにアクセスすることで「OWASP」を含む商品一覧を表示するショッピングサイトがあり、そこではレスポンスとして返されるHTMLの一部にinput要素のvalue属性の値としてURLのitemパラメータで指定された「OWASP」という文字列が含まれていたとします。

<form method="GET" action="/search">
    検索文字列: <input type="text" name="item" value="OWASP">
    <input type="submit" vaue="検索">
</form>

このときに、攻撃者が検索文字列として「" onmouseover="alert('hacked!')」という文字列を与えた場合に、サーバ側が以下のようなHTMLを返したとします。

<form method="GET" action="/search">
    検索文字列: <input type="text" name="item" value="" onmouseover="alert('hacked!')">
    <input type="submit" vaue="検索">
</form>

そうすると、input要素中ではvalue属性は空になり、onmouseover属性によってマウス移動時のイベントハンドラが設定されたHTMLとしてブラウザには解釈されます。
攻撃者によって誘導された利用者がブラウザでこのURLを開くと、onmouseoverイベントハンドラが有効であるためにinput要素の上でマウスを動かした瞬間にブラウザ内で攻撃者の作成したJavaScriptが動作してしまいます。
このように、テキストノードでは「<」「>」がエスケープされていないためにXSSが発生しましたが、属性値への出力では「"」(HTMLの書き方によっては「'」の場合もあり得ます)のエスケープがなされていないためにXSSが発生しました。

また、テキストノードと属性値以外にも、リンク先として設定されるURLにjavascript:スキームなどが埋め込まれた場合にもXSSが発生します。
例えば、自身のプロフィールの設定として任意のURLを登録できるSNSがあったとします。他の利用者がその人物のプロフィールページを表示させたときには、URLは自動的にリンクとしてa要素となるといったものです。

<div>
    <a href="https://www.owasp.org/index.php/Kansai">https://www.owasp.org/index.php/Kansai</a>
</div>

このような場合に、攻撃者が自身のプロフィールにURLとして「javascript:alert('hacked!')」といった文字列を設定した場合に、以下のようにそのままa要素のhref属性にこの文字列が設定されるとどうなるでしょうか。

<a href="javascript:alert('hacked!')">javascript:alert('hacked!')</a>

この攻撃者のプロフィールページを一般の利用者が閲覧し、そのときにリンク先のURLをクリックしてしまうと、攻撃者の作成したJavaScriptが利用者のブラウザ上で動作してしまいます。
このように、攻撃者が与えたURLによってリンクを生成する場合にjavascript:スキームなどを許容していると、エスケープ漏れの場合と同様にXSSが発生することになってしまいます。

XSSの対策

ここまで説明してきたとおり、XSSが発生する原因は以下の3種類が大部分を占めています。

  1. テキストノードに出力する際の「<」「>」のエスケープ漏れ
  2. 属性値に出力する際の「"」「'」のエスケープ漏れ
  3. URLをリンクとして取り扱う際のプロトコルスキームの確認漏れ

1.および2.はエスケープ漏れがXSSの原因であることは自明ですので、HTMLとして出力する際にこれらをエスケープします。エスケープすべき対象は以下の5文字です。
「&」は直接XSSの原因になるわけではありませんが、この文字をエスケープしなければユーザーから入力された「&」を正しく表示できませんので、エスケープが必要です。

< > " ' &

また、2.についてはエスケープに加えて、属性値を必ず「"」または「'」で囲むようにします。そうでなければ、"や'をエスケープしていたとしてもXSSの発生につながることがあるからです。多くのプログラミング言語やWebアプリケーション作成用のフレームワークでは、HTMLに出力するためにこれらの文字を適切にエスケープする関数があらかじめ用意されていることが多いでしょう。
PHPであれば htmlspecialchars 関数がそれにあたります。

<?php
    // GETで与えられたitemパラメータを取得
    $item = $_GET[ 'item' ];

    // 様々な処理
    ...
?>
<body>
    <!-- itemをエスケープしてHTML内のテキストノードに出力 -->
    <span><?php echo htmlspecialchars( $item, ENT_QUOTES, 'UTF-8' ); ?></span>
    ...

    <!-- itemをエスケープしてHTML内の属性値に出力 -->
    検索文字列: <input type="text" name="item" value="<?php echo htmlspecialchars( $item, ENT_QUOTES, 'UTF-8' ); ?>">
</body>

エスケープするのは、HTMLとして出力する直前です。外部から入力された文字列を受け取った直後にエスケープしてしまうと、その後その文字列を用いて様々な処理、例えば文字列の文字数に依存しているなどの処理を行う際に問題が発生しやすくなるからです。必ず、「HTMLとして出力する直前にエスケープ」を原則としましょう。
3.については、URLをリンクとして出力する際にはスキームがhttpあるいはhttpsであること、すなわちURLが「http://」または「https://」で始まることを確認するようにします。場合によっては「/foo」のように「/」で始まるURLを許容してもよいでしょう。*2
決して、javascript:スキームであればNG、のような確認を行わないようにしてください。XSSが発生するのはjavascript:スキームだけでなくvbscript:スキームを始め複数の種類があるため、「httpやhttpsのように安全なものだけを許可する」という方針のほうが堅実だからです。
例えば、PHPであれば以下のように正規表現を用いて文字列の先頭が「http://」「https://」あるいは「/」であることを確認し、そうである場合にのみa要素のhref属性にURLを設定することで、リンクによるXSSを防ぐことができます。

<?php
    function checkUrl( $url ){
        if( preg_match( "/\Ahttps?:\/\//", $url ) || preg_match( "/\A\//", $url ) ){
            return $url;
        }else{
            return "";
        }
    }
?>
...
<div>
    <a href="<?php echo htmlspecialchars( checkUrl( $url ), ENT_QUOTES, 'UTF-8' ); ?>">
        <?php echo htmlspecialchars( $url, ENT_QUOTES, 'UTF-8' ); ?></a>
</div>

ここでもa要素のhref属性の属性値として出力していますので、先ほど同様にhtmlspecialcharsによるエスケープを忘れてはいけません。

まとめ

ここまで解説したとおり、XSSの対策として有効な基本的なルールは以下のようなものになります。

  • テキストノードや属性値としてHTMLを出力する時点で「<」「>」「"」「'」「&」の5文字をエスケープする
  • 属性値は引用符で囲む
  • URLをリンクとして取り扱う場合にはhttpおよびhttpsスキームに限定する

これですべてのXSSが防げるわけではありませんが、現実に発生しているXSSの多くはこのルールを徹底することで防げるものが大半です。
本稿では、XSSの原理と対策の基本について解説を行いました。
一方、XSSは攻撃の種類や発生個所が多岐に渡るため、本稿だけではカバーできていない内容も多数あります。それらについては先に紹介したXSS (Cross Site Scripting) Prevention Cheat Sheetなどを活用してみてください。また、近年はJavaScriptによるクライアント側でのコード量が増えたことによって、JavaScript上で発生するDOM Based XSSと呼ばれる種類のXSSも増加しています。これについてはDOM based XSS Prevention Cheat Sheetなどを参考にしてみてください。

本稿がセキュアなWebアプリケーション構築の一助になれば幸いです。

*1:実際にはURLのitemパラメータはitem=%3Cscript%3E...のようになりますが、ここでは可読性を優先して便宜上URLエンコーディングを解いた状態で表記します。以降のURLに対する例も同様です。

*2:注2. 攻撃者の指定したURLをリダイレクト先として用いる場合には、リダイレクト先が許可されたドメインであるかを確認しオープンリダイレクタと呼ばれる脆弱性を防ぐ必要があります。オープンリダイレクタについての説明は本稿の範囲を超えますので今回は割愛します。