2021年3月25日木曜日

SQLインジェクションの対応について

Scott Spendoliniさんが昨年のAPEX@Home 2020にて、以下のセッションを行っています。

APEX Security Checklist
https://www.youtube.com/watch?v=b9esXRx2A-Q

Scott Spendoliniさんは、元々Oracle APEXのプロダクト・マネージャだったのですが、オラクルを辞めてOracle APEXのコンサルティングを行う会社を始めました。そして、昨年オラクルに戻り、今はOracle APEXを使ってアプリケーションを開発するチームを率いています。

2013年に、Expert Oracle Application Express Securityというタイトルで、Oracle APEXのセキュリティに関する書籍も出版されていて、この領域での専門家として認知されています。

このセッションの12:47から20:57まで、SQLインジェクションへの対応が説明されています。

このセッションで説明されている内容を、実際に確認してみました。

準備


SQLワークショップユーティリィからサンプル・データセットを呼び出します。

サンプル・データセットのEMP/DEPTインストールします。


言語Japaneseを選択し、へ進みます。


データセットのインストールを実行します。


サンプル・データセットのロードが完了しました。終了をクリックします。


続いて空のアプリケーションを作成します。名前は任意ですが、SQL Injection Testとしました。アプリケーションの作成を実行します。


アプリケーションが作成されたら、ホーム・ページをページ・デザイナで開きます。

Content Bodyにタイプクラシック・レポートのリージョンを作成します。タイトル従業員一覧とし、ソースのタイプをSQL問合せ、SQLとして以下を設定します。
SELECT * FROM EMP WHERE ENAME = '&P1_ENAME.'

ページ・アイテムP1_ENAMEの値を、置換文字列&P1_ENAME.を使って、WHERE句に指定しています。やってはいけないコーディングの例です。


ページ・アイテムP1_ENAMEをリージョン従業員一覧に作成します。名前P1_ENAMEタイプテキスト・フィールドラベル従業員名、(送信ボタンを追加するかわりに)[Enter]を押すと送信ONとします。


実際に実行されるSQLを確認するために、ソースに指定したSQLをヘッダー・テキストに指定しておきます。


以上で準備は完了です。これから、なぜこういうコーディングが問題なのか確認していきます。


置換文字列ではなく必ずバインド変数を使う


従業員名に中島 亜希子を入力し、レポートを表示します。


レポートを表示するSQLは&P1_ENAME.の部分が置き換えられ、以下のSQLが実行されています。
SELECT * FROM EMP WHERE ENAME = '中島 亜希子'

従業員名を入力している限り、問題は見つかりません。

以下を従業員名に入力してみます。

中島 亜希子' or '1'='1

実行結果は以下になっています。表EMPの全行が表示されています。

実際に実行されているSQLは以下です。

SELECT * FROM EMP WHERE ENAME = '中島 亜希子' or '1'='1'

全行選択される条件になってしまっていることがわかります。

従業員のSAL(給与)、COMM(コミッション)はレポートに表示する列から外すことにします。

ソースのSQLを以下に変更します。

SELECT EMPNO, ENAME, JOB, MGR, HIREDATE, DEPTNO FROM EMP WHERE ENAME = '&P1_ENAME.'

先程の条件であれば、全従業員がリストされます。SALおよびCOMMは表示されません。

従業員名として以下を入力します。

中島 亜希子' UNION SELECT EMPNO, ENAME || ' - ' || SAL || ' - ' || COMM ENAME, JOB, MGR, HIREDATE, DEPTNO FROM EMP WHERE '1'='1
ENAMEとして、SALとCOMMが表示されていることが確認できます。


実行されたSQLは以下です。
SELECT EMPNO, ENAME, JOB, MGR, HIREDATE, DEPTNO FROM EMP WHERE ENAME = '中島 亜希子' UNION SELECT EMPNO, ENAME || ' - ' || SAL || ' - ' || COMM ENAME, JOB, MGR, HIREDATE, DEPTNO FROM EMP WHERE '1'='1'

こうなると、どのようなSELECT文も実行でき、データベースの接続ユーザーにSELECT権限があれば、何でもデータとして取得できる状態です。

例えば以下を従業員名に指定すると、データベースに登録されているユーザーの一覧を取得できるはずです。(ビューALL_USERSのSELECT権限が必要です。)。結果を載せるのは控えます。

中島 亜希子' UNION SELECT null, USERNAME, null, null,null,null from ALL_USERS where '1'='1

従業員名として以下を指定すると、スキーマにどのような表があり、どのような列を持っているかを確認することができます。

中島 亜希子' UNION SELECT null, TABLE_NAME, COLUMN_NAME, DATA_LENGTH,null,null from USER_TAB_COLS where '1'='1

これも結果を載せるのは控えます。

これほど危険なコーディングなので、SQLに置換文字列は絶対に使うべきではありません。

SQLを記述する際には置換文字列ではなく、必ずバインド変数を使います。

ページ1のコピーとしてページ2を作成し、ソースのSQL(とヘッダー・テキスト)を以下に変更します。

SELECT EMPNO, ENAME, JOB, MGR, HIREDATE, DEPTNO FROM EMP WHERE ENAME = :P2_ENAME

従業員名として、SQL文やSQL文の一部を入力しても、評価されるSQL文自体は変わらないことが確認できます。また、入力値は必ず従業員名として評価され、一致する従業員は存在しないため、一行も結果が返されないことが確認できます。


SQL問合せを返すファンクション本体ではDBMS_ASSERTを使う


ソースのタイプがSQL問合せであれば、バインド変数を使うことで、実行されるSQLが入力値によって置き換わることは起こりません。

ソースのタイプとしてSQL問合せを返すファンクション本体を選んだ場合は、SQL文はファンクション内で動的に構築します。ですので、バインド変数を使っていてもSQLインジェクションを許してしまうことがあります。

先程のページ2をコピーし、ページ3を作成します。ソースタイプSQL問合せを返すファンクション本体に変更し、SQL問合せを戻すPL/SQLファンクション本体として以下のコードを設定します。

declare
    l_sql varchar2(4000);
begin
    l_sql := 'SELECT EMPNO, ENAME, JOB, MGR, HIREDATE, DEPTNO FROM EMP WHERE ENAME = ''' || :P3_ENAME || '''';
    return l_sql;
end;

ページ・アイテムP3_ENAMEを参照する際にバインド変数を使用していますが、置換文字列を使ったときと同じ動作になります。

以前に使用した例である、SALとCOMMをENAMEに表示する文字列を従業員名に与えると、SAL、COMMともに表示されます。


タイプとしてSQL問合せを返すファンクション本体を選択しているので、バインド変数を使用していても、最終的にはSQL文自体にその変数の内容が含まれることになります。安全にバインド変数の内容をSQL文に含めるために、DBMS_ASSERTパッケージに含まれるファンクションを使います。

ページ3をページ4としてコピーし、SQLを生成するコードを以下に変更します。
declare
    l_sql varchar2(4000);
begin
    l_sql := 'SELECT EMPNO, ENAME, JOB, MGR, HIREDATE, DEPTNO FROM EMP WHERE ENAME = '
    || SYS.DBMS_ASSERT.ENQUOTE_LITERAL(:P4_ENAME);
    return l_sql;
end;
:P4_ENAMEのクオートで囲む処理を直書きするかわりに、DBMS_ASSERT.ENQUOTE_LITERALファンクションを呼び出します。

先程と同じ従業員名を与えると、ORA-6502が発生します。

DBMS_ASSERTパッケージの説明はこちらです。ENQUOTE_LITERALによって、先行および後続文字を除く、すべての一重引用符が対を成していることが検証されます。

DBMS_ASSERTの先頭にSYSをつけることにより、カレント・スキーマに同名のパッケージがあるとそちらが優先されることを回避しています。

経験上、SQLに含まれる列や表が変わる場合は動的にSQLを作成しないと対応できませんが、WHERE句については、そのまま条件を書ける場合が多いです。できれば、SQL問合せを返すファンクション本体は使わないのが望ましいです。

PL/SQL言語リファレンスも参照する


Oracle APEXでのSQLインジェクションの危険と対応方法について説明してきました。より一般的な説明が、PL/SQL言語リファレンスに含まれています。


こちらの解説も一読をお勧めします。

Oracle APEXで作成したアプリケーションの脆弱性をレビューするツールとして、サードパーティーよりApexSecというツールが提供されています。私は使ったことがありませんが、USのOracle APEX開発部門でも使用していると伺っているので、検討に値するツールだと思います。

また、SQLにはバインド変数を必ず使うか、SQL文を動的に生成する際にはDBMS_ASSERTパッケージを呼び出せば対応できるとしても、それがアプリケーションに徹底できるかどうかは別の話になります。Virutal Private DatabaseまたはReal Application Securityを導入することにより、アプリケーションからどのようなSQLが発行されても、権限が与えられていない処理は実行しないように制限をかけることができます。RASについては以前に記事を書いていますので、そちらの方も参照していただければと思います。

以上になります。

Oracle APEXの安全なアプリケーション作成の参考になれば幸いです。

2021年3月24日水曜日

リモート・デバッグの方法の紹介

 カナダのInsumというパートナーさんが、APEX Instant Tipsというタイトルで、ちょっとしたOracle APEXの使い方を紹介しています。

その12回目にオラクルのJoel Kallmanさんが招待され、リモート・デバッグについて紹介していました。

録画はこちらより視聴できます。

Episode 12: How to remote debug a user's session

https://www.insum.ca/apex-remote-debug-user-session/

そこで説明されていた内容と、少々デバッグに関する情報を紹介します。

例えば利用者がOracle APEXで作られたアプリケーションにアクセスしていて、何か問題が発生したとします。電話やチャットで開発者にサポートが求められている状況を想定します。

開発者はアプリケーション・ビルダーにサインインすることができます。

開発者は、連絡を受けて最初に利用者の名前を確認するでしょう。

名前、正確にはユーザー名の確認ができれば、そのユーザーのセッションを見つけることができます。

アプリケーション・ビルダーの右上の管理(スパナと人のアイコン)アイコンからメニューを開いて、アクティビティのモニターを実行します。


アクティビティのモニターアクティブ・セッションがあります。これを開きます。


アクティブ・セッションを開くと、その時点で有効であるセッションの一覧がユーザー名(レポート上は所有者)とともに表示されます。ユーザー名が分かっていれば、セッションが作成(開始)されたタイミングや最新のビューから、どのセッションなのかは大体見当がつけられるでしょう。そのアクティブ・セッションをクリックして開きます。


そのセッションの詳細が表示されます。そのユーザーがOracle APEXのアプリケーションに対して、どのような操作を行ってきたか、ページ・ビューとして一覧されます。


発生しているエラーや、それがどのページの何の処理で発生したのかを確認することができます。利用者にエラーメッセージなどを確認する必要はありません。

エラーが発生している場合は、デバッグIDをクリックして実行ログを確認します。


この時点で十分な情報が出力されていればよいですが、解析するには情報が足りない場合もあるでしょう。その場合はアクティブ・セッションに戻ってデバッグ・レベルを変更します。

例えばデバッグ・レベル情報(LEVEL4)に変更します。変更の適用をクリックします。利用者に問題が発生したオペレーションを再実行してもらえば、より詳細な実行ログを取得できます。


デバッグ・レベルを情報まであげると、ページ・アイテムに設定された値などもログに含まれるようになります。(ちなみにトレース・モードはSQLトレースをファイルに出力するためのオプションで、alter session set sql_trace = trueと同じです。データベース管理者の協力がなければ、ほぼ使うことのない指定です。)


パフォーマンスの問題などで実行計画を確認したい場合は、デバッグ・レベルAPEX Trace(LEVEL9)まで上げます。


デバッグ・レベルをAPEX Traceまで上げると大量の実行ログが出力されます。その中に実行計画の出力も含まれます。


速度は極端に遅くなるので、必要な情報を取得した後はデバッグ・レベルは下げた方がよいでしょう。

続いて注意点です。アプリケーション定義でデバッグがONになっていると、アプリケーションの利用者によってデバッグを有効にすることが可能になります。

例えばf?p形式のURLの場合は

f?p=アプリケーションID:ページID:セッションID:リクエスト:デバッグ:::

簡易URLの場合は

debug=デバッグ

です。アプリケーションのURLでデバッグが指定されていると、リモートからの設定より優先されます。本番アプリケーションでは、ブラウザからデバッグを有効にできるようになっていると問題があるので、デバッグOFFにします。


デバッグに関するマニュアルの記載はこちらです。

最後に開発したアプリケーション固有のデバッグ・ログを出力するために、APEX_DEBUGパッケージを使用できます。ERROR、WARN、INFO、TRACEといったデバッグ・ログを出力するプロシージャが含まれています。

プロセスなどに比較的長いコードを記述する場合に、APEX_DEBUG.INFOプロシージャを呼び出して、デバッグを容易にするメッセージを出力することをお勧めします。

今回紹介したアクティビティのモニターには、アプリケーション・エラーという項目もあります。


アプリケーション・エラーを開くと、発生したエラーの一覧を確認できます。


発生しているエラーは、利用者からの報告の有無にかかわらず確認することができます。特に頻繁にアプリケーションを置き換え、利用者に置き換えについて通知しない運用をしている場合に有用です。

セッションやデバッグ・ログを確認するためにアプリケーション・ビルダーにサインインするのは手間であったり、または、サポート要員が確保できていれば、アプリケーションをデバッグするためのアプリケーションを開発することも可能です。

セッションの情報はビューAPEX_WORKSPACE_SESSIONS、ページのアクセスは、ビューAPEX_WORKSPACE_ACTIVITY_LOG、デバッグ・ログはビューAPEX_DEBUG_MESSAGESから取得できます。

アプリケーションが自分自身のセッションのデバッグを有効/無効にするためにAPEX_DEBUG.ENABLE/DISABLEが使えます。異なるセッションの場合はAPEX_SESSION.SET_DEBUGを呼び出します。

Oracle APEXでは、フレームワーク側でセッションの情報、アクセス履歴やエラー・メッセージなどを記録してくれますし、保存されている情報はSQLで取り出すこともできます。ですので、デバッグ用のコードの埋め込みを少なくすることができます。

PL/SQLのコーディングなどを学ぶと、パッケージを使う、例外処理をきちんと書く、ということが推奨されています。確かにそうなのですが、Oracle APEXに埋め込むコードについていえば、例外をトラップしていなければ、アプリケーション・エラーとしてメッセージが記録されます。

APEXにはアプリケーションのデバッグを容易にするための機能が備わっているので、それを活用すると良いのではないかと思います。そうすることで、短期間でとりあえず使えるアプリケーションを作ることができます。

2021年3月23日火曜日

CSVでアップロードした行の差分を確認する

 データ・ロード・ウィザードでCSVファイルを繰り返しアップロードしたときに、直近のアップロードに含まれていない行を確認したい、との相談がありました。 データ・ロード・ウィザードの場合、主キー項目がなければ新規行として挿入、あればその行が更新されますが、アップロードしたデータに含まれない行はそのまま残ります。

そのまま残っている行を対話モード・レポートでハイライトさせます。

アップロードする前にデータを全削除(またはバックアップを取って全削除)する、トリガーを追加し、新規行と更新行にフラグを立てる、といった方法も考えられますが、今回はオラクル・データベースの機能であるORA_ROWSCN疑似列を使ってみることにしました。

アップロードするCSVファイルは以下から取得します。

https://apex.oracle.com/pls/apex/japancommunity/r/simcontents/download?id=Project_and_Tasks_ja.csv

上記のCSVファイルを取り込む表をクリックSQLのモデルとして定義します。
# prefix: hld
# semantics: default
tasks
    project vc255
    task_name vc255
    start_date
    end_date
    status vc8
    assigned_to vc80
    cost num
    budget num
SQLの生成を実行し、SQLスクリプトの保存をしたのち、レビューおよび実行を行います。


表HLD_TASKSを作成するCREATE文の末尾にROWDEPENDENCIESを追加し、ORA_ROWSCN疑似列が、それぞれの行の変更時のSCNを持つようにします。デフォルトはNOROWDEPENDENCIESで、ORA_ROWSCNは行ではなくブロック単位での変更時のSCNを保持します。


CREATE文を実行し表HLD_TASKSを作成したのち、アプリケーションの作成を実行し、アプリケーション作成ウィザードを起動するところまで進みます。名前削除行確認とし、アプリケーションの作成を行います。対話モード・レポートのページは、アプリケーション作成ウィザードが作成します。


アプリケーションが作成されたら、データ・ロード・ウィザードのページを作成します。ページの作成を開始します。


コンポーネントデータのロードを選びます。


定義名は任意ですが、今回はタスクとしました。表名HLD_TASKS一意列(の列1)にはID (Number)を選択し、へ進みます。


トランスフォーメーション・ルール表ルックアップ何も定義せず、そのままへ進みます。ページ属性もデフォルトをそのまま使用し、へ進みます。


サイド・メニューよりデータ・ロード・ウィザードを開始できるよう、ナビゲーションのプリファレンスとして新規ナビゲーション・メニュー・エントリの作成を選択します。に進みます。


「取消」ボタンでブランチするページ、および、ページへの「終了」ボタン・ブランチ、つまり対話モード・レポートのページに設定します。作成をクリックします。


データ・ロード・ウィザードのページが追加されたので、アプリケーションを実行してCSVファイルをロードします。


アプリケーションにサインインし、サイド・メニューよりデータのロードを実行します。インポート先としてファイルをアップロード(カンマ区切り(*.csv)またはタブ区切り)を選びます。1行名に列名があるをチェックします。詳細設定の使用にチェックを入れて、ファイルの文字セットとして日本語(Shift_JIS)を選択します。設定を終えたらに進みます。


データと表のマッピングを確認してに進みます。(表の列名とCSVの先頭行の列名は一致するようにしているため、自動的にマッピングされます)


データ検証のページが開きます。アクションはすべて行の挿入で、全部で73件になっています。に進みます。


データ・ロード結果として、挿入された行73であることを確認し、終了します。


対話モード・レポートのページが表示されます。データがロードされていることが確認できます。


挿入および更新された行と削除された行を示す列ROW_STATUSを追加します。追加する列の定義は以下になります。一番最近に更新された行についてはUを返し、それ以外はDを返します。対話モード・レポートや他の操作で行が更新されると、その行が最新のSCN番号を持つため、列ROW_STATUSに意味が無くなります。列ROW_STATUSはデータ・ロード・ウィザードの実行直後で、その後にデータが更新される前まで有効です。
       case
       when ora_rowscn = (select max(ora_rowscn) from hld_tasks) then
       'U'
       else
       'D'
       end row_status
対話モード・レポートのソースタイプSQL問合せに変更し、以下のSQLを設定します。
select ID,
       PROJECT,
       TASK_NAME,
       START_DATE,
       END_DATE,
       STATUS,
       ASSIGNED_TO,
       COST,
       BUDGET,
       case
       when ora_rowscn = (select max(ora_rowscn) from hld_tasks) then
       'U'
       else
       'D'
       end row_status
from HLD_TASKS

直前にアップロードされたCSVに含まれていた行については、列ROW_STATUSUと表示されます。


最初にロードしたCSVファイルには列IDが含まれていません。列IDを含んだCSVファイルを作成するため、対話モード・レポートよりダウンロードを実行します。

最初に列IDをレポートの表示に含めます。アクションからを呼び出します。


IDを表示しないからレポートに表示へ移動します。


アクションからダウンロードを呼び出します。


レポートのダウンロード形式の選択としてCSVを選び、ダウンロードを実行します。


ダウンロードしたファイルから行を削除し、再度、データ・ロード・ウィザードを使ってアップロードします。

アップロードが完了したら、アクションの書式からハイライトを実行します。


ハイライト条件として列ROW_STATUS = Dを設定します。


フィルタ条件を適用すると、本記事の最初の画像のように、削除された行がハイライト表示されます。

以上でアプリケーションの作成は完了です。

ROWDEPENDENCIESを有効にすると表圧縮がサポートされない、とのことなので、その点は要注意です。

今回作成したアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/highlightdeletedrows.sql

Oracle APEXのアプリケーション作成の参考になれば幸いです。