Webブラウザに対するJavaScriptコードのインジェクションは
- サーバー側のコードが原因(サーバー側のPHP、Ruby、Python、JavaScriptが原因)
- クライアント側のコードが原因(クライアント側のJavaScriptが原因)
- サーバーとクライアント(上記の両方)
で起きる可能性があります。ここでは主にクライアント側が関係するケースで注意しなければならない箇所を紹介します。
どこが危険なのか?
ブラウザ上のJavaScriptの場合、ECMAScriptの基本として実装されている機能、DOM機能として実装されている機能があります。更にこの他にもHTMLやCSS機能もあり複雑です。SVGなどこの他の標準にもJavaScriptが記述できるモノがあります。
JavaScriptを実行可能となる部分へユーザー入力データを出力する箇所すべてに危険性があります。
ここに挙げている機能が全てではありません。JavaScriptは組み込みObjectの拡張も可能なので、使用しているライブラリ/フレームワークによって危険なコードが変わります。
基本的にはHTMLやHTMLの一部、JavaScriptやJavaScriptの一部を生成するコードで”コンテクストに合ったエスケープ”を行わない機能すべてが危険です。
HTML中に記載されるモノで特にリスクが高いモノ:
- href属性 – URIデータににはURIエスケープが必要。プロトコルバリデーションも必要。
- src属性 – URIデータににはURIエスケープが必要。プロトコルバリデーションも必要。
- <script>タグの中身全て
- onclick、onmouseoverなど
※1 イベントハンドラー (”The event occurs”で始まるモノ全て)Mozilaのイベントハンドラーマニュアル、分かりづらい物にseekSegmentTime()、FSCommand()がある。setAttribute()でもイベントハンドラーを登録できる。
※2 イベントハンドラーは”プログラムを直接記述”します。eval、<script>タグの中身と変わりません。hrefやsrcにもプログラムを記述可能です。ブラックリスト型で丸ごとエスケープやしても意味がない。サニタイズ(危険な文字は廃除)も危険性が高い。厳格にホワイトリスト型で入力バリデーションを行う方が安全性が高い。
※3 エスケープする場合、元の入力が攻撃用にエンコーディングされていることを想定し、全てUnicode文字参照にエンコーディングする方法が安全。
※4 識別子などが変数の場合は必ず厳格にバリデーションする。本来は変数値もバリデーションする。
※5 href/src属性にはjavascriptやdataプロトコルも使える(プログラムが記述できる)点に注意。プロトコルをチェックする場合、許可する物であるかバリデーションする(ホワイトリスト型検証)
JavaScript基本機能で危険な機能/関数など:
- “+” (文字列連結)
- 変数定義
- innerHTML()
- outerHTML()
- write()
- writeln()
- location()
- url()
JavaScriptは基本オブジェクトの拡張も可能です。JavaScriptライブラリやフレームワークを利用する場合はこれらにも注意が必要です。
- .html
- window.__PRELOADED_STATE__
- etc, etc
onclickなどのJavaScriptを実行するイベントハンドラーと同じで、JavaScriptを実行する機能に対してJavaScript文字列用のエスケープを行ってもリスクがあります。(パラメータを丸ごとエスケープをしても意味がない)
- eval()
- setInternval()
- function()
- constructor
JavaScriptコード内でスクリプトになるオブジェクトを生成してDOM要素に追加する、といった操作をすると危険になる場合もあります。
インジェクションリスクが無いモノ:
- JSON – JSONにeval()を使うと勿論危険。JSON.parse()を利用
- CSS – ただし古いブラウザにはインジェクションリスクがある
JSONもCSSもHTML埋め込みの場合、JavaScriptインジェクションが可能です。JSON文字列にはJavaScript文字列エスケープ、CSSにはCSS用エスケープを利用します。エスケープできないモノはバリデーションします。
※1 JSONPは利用禁止。
※2 古いブラウザやIEにはリスクがあります。IE11もexpression()をサポートしているがInternetZoneの場合はデフォルトでは無効。
※3 不正な値でも問題となる誤作動をしないことを保証するのはかなり困難です。正しく実行するには、正しい値が必要なので、正しい値であることを保証する方が良いです。
JSONデータも数値のバリデーション、文字列データのエスケープ(JavaScript文字列変数のエスケープと同じ)を行わないと余計な要素を追加されるリスクがあります。ただし、余計な要素がないことサーバー側でもチェックしなければならない。
CSSにもCSS用のエスケープ方法(\HH形式)が定義されています。CSSも攻撃に利用されるので、エスケープとバリデーションを用いて確実に余計な操作が行われないようにコーディングする必要があります。
識別子や構文語句について
識別子(変数名やタグの属性名など)や構文語句がパラメーターになっている場合、エスケープは意味がない場合があります。これらがパラメーターである場合に安全に出力する機能は通常は存在しません。
任意の識別子を設定できるようにしてしまうと、思わぬ誤作動の原因になります。基本的にこれらが変数になっている場合、安全性を確実に保証できる限定的なバリデーションを行います。
文字列連結について
どの言語でも同じですが文字列連結が危険になるケースは、プログラムから文字列(JavaScriptやSQLなどの意味を持つ文字列。一文字でも意味がある場合、インジェクションリスクの考慮が必要)を生成して処理させる場合にリスクが発生します。
典型的なNG例:
var v = '<b>' + unvalidated_nor_escpaed_var + '</b>' var url = 'https://example.com/'+ unvalidated_nor_escpaed_var
他の言語から出力もNG:
var v = str1 + <%= unvalidated_nor_escaped_input %>
変数や引数を丸ごと設定もNG:
var v = <%= unvalidated_nor_escaped_input %> some_function(<%= unvalidated_nor_escaped_input %>)
安全に出力する方法
HTMLやJavaScriptとして解釈されるコンテクストに出力する場合、次の方法があります。
- コンテクストに合ったエスケープを行う – HTML、URI、CSS、JavaScript文字列変数でエスケープ方法が異なる
- 自動的にエスケープされるAPIを使用する – .innerHTML()ではなく.innerText()など
- 出力データが安全である事を保証するバリデーションを行う – 識別子(JS変数はHTMLタグ属性名など)をエスケープしてもあまり意味がない。識別子用の無害化API普通は無い。
※1 入力/ロジックでバリデーション済みである場合、フェイルセーフとしてのエスケープが推奨される。例えば、クエリ文字列の識別子(変数名)のエスケープはインジェクション防止策として機能する。
参考:出力対策の3原則+1原則
コードを実行するような機能(eval、イベントハンドラーなど)の引数を丸ごとエスケープしても意味がない点には注意する必要があります。
エスケープする場合、確実に安全な形式(Unicodeエスケープ形式など)にエンコードする方が良いです。
JavaScriptライブラリを安全に利用する方法
JavaScriptライブラリの安全性を保証するには、ソースコードを読んで安全性を検証するしかありません。ライブラリの中には仕様としてエスケープを省略しているモノも少くありません。
JavaScriptプログラムコードが含まれない入力データであること保証すると、より安全にJavaScriptライブラリを利用できます。多くの入力は整数だったり名前だったり、コードで無いこと保証できる物が多くあり、そういったデータの方が安全に処理されていないケースが多いです。
ブラックリスト型対策はNG
ブラックリスト型対策はエンコーディングで簡単に回避される可能性がある。また実装によってはエンコーディングを使わなくても”無視される文字”を挿入することによりブラックリスト型対策を無効化できる場合があります。
< %3C < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < < \x3c \x3C \u003c \u003C
現在のUnicode標準では最も短い表現のみが”妥当”なエンコード方法と定義されています。これらの不正な形式はUnicodeエンコードをバリデーションするコードが検出できれば問題を廃除できます。
しかし、形式が異なるエンコード方式もあるので、これを悪用されるとブラックリスト型のセキュリティフィルタを回避される可能性が残ります。
バリデーションについて
エンコーディングされているデータをバリデーションする場合、デコードしてから検証します。重複したデコードは行ってはならないです。余計なデコードは攻撃の入り口になります。
JavaScript文字列ならJavaScript文字列変数に格納された時点で、JavaScript文字列用のエスケープ(エンコード)はデコードされています。HTMLコンテクストに出力されている文字列ならHTML文字参照などもデコードされています。
バリデーションする際、記号を含めてしまうとエンコードによりバリデーションの効果が無効化される場合があります。それが入力仕様なら仕方ありません。出力時に無害化できていることを保証できるコードにします。
バリデーションの効果を最大限にするためには、特別な意味を持つ文字を含めてしまう弱いバリデーションではなく、英数字のみで長さ10文字まで/特定の識別子名のみ、といった強いホワイトリスト型のバリデーションを行います。
コンテクストの区別について
JavaScriptでの出力に限らず、出力先のコンテクスト、が何であるか?どういうセキュリティ処理がされているのか?を意識する必要があります。
例えば、ブラウザのJavaScript基本機能として組み込まれているinnerHtmlとinnerTextの違いです。前者は入力をHTMLと解釈するためエスケープが必須で後者は自動的にエスケープ処理をします。
JavaScriptフレームワークを利用する場合でコンテクストを理解するテンプレートシステムを持っている場合(Reactなど)はHTML、URI、CSSといったコンテクストに応じたエスケープ処理を行います。しかし、テンプレートシステムの外ではこういった処理が自動的に行われるとは限りません。
出力している場所のコンテクストが何処で、何をしなければリスクが発生するのか、何をするとリスクがリスクが発生しないのか意識し、必要な処理に漏れが無いように注意しなければなりません。
複数の出力先コンテクストを持つ場合、出力先のコンテクストでデコードされるエンコード形式が異なる点に注意が必要です。これは安全に出力可能かバリデーンする際、特に重要です。
識別子の取り扱い
基本としてはエスケープされていない文字列連結(と代入)は全て危険と考えるべきです。しかし、JavaScriptの変数名(識別子)となる文字列の場合はあまり問題になりません。
例えば、DOM要素を取得する場合にタグやIDなどに不正な値が設定されても、要素が見つからないだけです。見つからない事が問題になることは少ないです。(アプリケーションとしておかしな状態になりますが、直接インジェクション問題にはならない。IDのプレフィックスに追加の連番を付ける、といった操作は通常安全です。例:$(‘some_prefix’ + var)
しかし、攻撃者がDOM要素の中に攻撃用データを設定でき、そのDOM要素をJavaScriptなどが解釈されるコンテクストに投入できる、といった場合には問題になります。
- 識別子が見つからないことはほとんど問題になることはない(プレフィックスなどがユニークで、その範囲で自由にアクセスしてもOKなら問題ない)
- ユーザー入力により任意の識別子が指定できるようなコードはNG(最初は安全でもページの変更でいつ危険な状態に変わるか分らない)
クライアント側でのインジェクション防止参考資料
- CERT Top 10 Secure Coding Practices
- DOM based XSS Prevention Cheat Sheet
- AJAX Security Cheat Sheet
- XSS (Cross Site Scripting) Prevention Cheat Sheet
- XSS Filter Evasion Cheat Sheet (現在のブラウザでは無効な物も多い)
エスケープ
- PHPのエスケープ
- JavaScriptのエスケープ – Function propertiesに記載されているglobal function。escape()はDeprecated。代わりにencodeURI、encodeURIComponentを利用する。HTMLエスケープやJavaScript文字列用エスケープ関数などはデフォルトで用意されていない。
JavaScript文字列用にエスケープする場合、ASCIIのプリンタブル文字も含め、全ての文字をUnicodeエスケープ形式にエンコードする方法が最も安全性が高いです。
バリデーション
クライアント側でも不正な入力データで誤作動しないようにしなければならないです。しかし、クライアント側のバリデーションはあくまでクライアント側のプログラムが正しく動作する為のバリデーションです。
サーバー側はサーバー側で別途に入力データが正しいデータであることをバリデーションしなければなりません。
入力バリデーションはできる限り限定的で厳しく検証するようにします。出力時の安全性確保の為にバリデーションする場合、出力コンテクストで意味を持つ記号文字などを含むバリデーションでは意味が無くなります。
まとめ
JavaScriptコードを埋め込こむケースとしてサーバー側で生成するケースも含めたので、解りづらいかも知れません。しかし、無害化していない「変数の埋め込み」はどちらで行っても危険なので同様に考える方が解りやすい側面もあると思います。
JavaScript環境での注意点はとても多いので書ききれません。当たり前と思っている事、レアケース、解説する長くなる物などは省略しています。
JavaScriptの場合、関連するDOMやサーバーサイドでの処理が安全性に影響するので簡潔にまとめることが難しいです。あまりまとまりが良くないので別のアプローチでまた書くかも知れません。
コンテクストを理解し、安全であることを保証するコードになっていればOKです。ほとんどの場合、コンテクストの危険性の見落しかうっかりミスで余計なJavaScriptやHTMLが埋め込まれる問題が発生します。厳格な入力バリデーションは見落し、うっかりミスによる問題発生を効果的に防止します。