buri.memo.show();

アイデアや気づきとかが雑に書き殴られる

XSSのエスケープだけで防げないケース、その対策とテスト方法

XSS (クロスサイトスクリプティング)とは Web サイトの代表的な脆弱性。有名なだけあって実際 XSS の脆弱性があるだけでマジでやばいが、その割に対策は意外と簡単じゃなかったりする。

脆弱性自体のヤバさは他の記事に説明してもらう事にして、エスケープだけでは防げないケースが面白かったのでまとめてみる。最後に簡易的なテスト方法も紹介する。

エスケープ(サニタイジング)による基本的な対策

XSS を防ぐ根本的対策として良く挙げられるのは攻撃に使われる HTML の特殊文字をサーバーサイドでレンダリングする際にエスケープして「ただの文字」にしてしまうこと。

& < > " ' → escape → &amp; &lt; &gt; &quot; &#039;

基本的にはこれによって多くのケースを防ぐことはできるが、防げないものもある。

以降は jinja みたいなテンプレートエンジンを使用して HTML を生成する際に、{{ data }} と書くと data に入っている値が(自動的に)エスケープされて埋め込まれると思って読んでほしい。

Template Designer Documentation — Jinja Documentation (3.1.x)

エスケープするだけでは防げないケース

script タグの内側にレンダリングした場合

script タグの内側に埋め込まれた場合、エスケープされる文字が含まれていなくても攻撃コードを注入できる。以下のようなコードでは、data に alert() が渡されるとダイアログが表示される。

埋め込む場合は必ず "{{ data }}"'{{ data }}' の様に囲ってあげる必要がある。

<script>
    const userName = {{ data }};
    // userName を使う処理
</script>

「"」や「'」無しに任意のタグの属性としてレンダリングした場合

例えばフォームの初期値として入れるために input タグの value にレンダリングする事がある。

以下の様に書かれていて、 data に 1 onmouseover=alert() style=position:absolute;top:0;left:0;height:4000;width:4000 が渡された場合、ブラウザにカーソルが乗った瞬間にダイアログが表示される。

こちらも対策としては "{{ data }}"'{{ data }}' と囲ってあげる必要がある。

<form>
    <input type=text value={{ data }}>
</form>

a タグの href 属性にレンダリングした場合

a タグの href には通常 URL を入れるが、data に javascript:alert() と javascript: から始まる文字列を入れることで、クリックされたときに JavaScript として解釈されて実行される。

このケースでは http: から始まらない URL は無効化(例えばブランクに置換してしまう)するという対策がとれる。

<a href="{{ data }}">テスト</a>

(IE5, IE6, IE7 のみ) スタイルシートがレンダリングできる場合

流石に問題視されたのか、殆どのブラウザでは動かない。

IE5~IE7 では CSS の expression() に JavaScript を埋め込むことができたらしい。data に left:expression(alert()) を渡すことで ダイアログが表示される(環境が無くて試せなかったけど)。

<div style="{{ data }}">テスト</div>

DOM-based XSS

若干趣旨と異なるが、JavaScript を使ったクライアントサイドレンダリング時にも XSS に脆弱になる場合がある。サーバーサイドでのレンダリング時にエスケープが十分できていてもクライアントサイドが疎かだと問題になってしまう。

以下の様に JavaScript でデータをとってきた後、innerHTML (もしくは outerHTML やライブラリやフレームワークの、直接 HTML を生成できる機能)を使ってレンダリングすると、ダイアログが表示されてしまう。ちなみにinnerHTMLで直接 script タグを埋め込んでも動かないようになっている(HTML の仕様)。

対策としては、innerHTML の代わりに textContent を使用するか、事前にエスケープ処理を行う必要がある。さらに、エスケープする場合も前述のケースに気を付ける必要がありそう。

<p id="info"></p>

<script>
    fetch("http://example.com/").then((response) => {
        // "<img src='x' onerror='alert()'>"
        document.getElementById("info").innerHTML = response.text();
    })
</script>

テスト方法

レンダリング時にエスケープを完全に施したとしても、挿入位置によっては XSS 攻撃を防げない場合があることが分かった。

簡易的なテスト方法としては紹介してきたような攻撃コードを入れてみて、alert が出たりタグが壊れ HTML の表示が崩れないことを確認するということができる。

以下にいくつか入力パターンを載せておく(網羅性は保証しないけど)

テストパターン

エスケープ漏れ

"><script>alert()</script><!--
'><script>alert()</script><!--
</script><script>alert()</script><!--
<img src=x onerror=alert()>

エスケープしていても脆弱になるケース

alert()
1 onmouseover=alert() style=position:absolute;top:0;left:0;height:4000;width:4000
javascript:alert()

参考