Scott Spendoliniさんが昨年のAPEX@Home 2020にて、以下のセッションを行っています。
APEX Security Checklisthttps://www.youtube.com/watch?v=b9esXRx2A-Q
準備
データセットのインストールを実行します。
SELECT * FROM EMP WHERE ENAME = '&P1_ENAME.'
ページ・アイテムP1_ENAMEをリージョン従業員一覧に作成します。名前はP1_ENAME、タイプはテキスト・フィールド、ラベルは従業員名、(送信ボタンを追加するかわりに)[Enter]を押すと送信はONとします。
置換文字列ではなく必ずバインド変数を使う
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
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ともに表示されます。
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;
先程と同じ従業員名を与えると、ORA-6502が発生します。
DBMS_ASSERTの先頭にSYSをつけることにより、カレント・スキーマに同名のパッケージがあるとそちらが優先されることを回避しています。