PL/SQL の動的 SQL でテーブル名や列名を変数として組み立てる場合、バインド変数(:bind_var)でオブジェクト名を渡すことができないため、文字列連結で SQL を組み立てるしかありません。このとき入力値を適切に検証しないと SQL インジェクションの脆弱性が生じます。
DBMS_ASSERT は Oracle が提供する入力値検証パッケージです。テーブル名・スキーマ名・列名として適切な値かどうかを検証し、不正な入力に対して例外を発生させます。動的 SQL のセキュリティ対策の基本ツールです。
この記事でわかること
- DBMS_ASSERT が必要な理由と SQL インジェクションのリスク
- ENQUOTE_NAME でオブジェクト名(識別子)を安全にダブルクォートする方法
- ENQUOTE_LITERAL で文字列リテラルをシングルクォートでエスケープする方法
- SCHEMA_NAME / SQL_OBJECT_NAME / QUALIFIED_SQL_NAME で識別子を検証する方法
- SIMPLE_SQL_NAME で簡単な識別子構文を検証する方法
- EXECUTE IMMEDIATE と組み合わせた安全な動的 SQL の書き方
DBMS_ASSERT が必要な理由
EXECUTE IMMEDIATE でテーブル名を動的に組み立てる場合、バインド変数は使えません。
SQL インジェクションが発生する危険なコード例
-- NG: テーブル名を検証なしに直接連結する(SQLインジェクションの危険あり)
CREATE OR REPLACE PROCEDURE unsafe_count(p_table_name VARCHAR2) AS
v_sql VARCHAR2(500);
v_cnt NUMBER;
BEGIN
-- p_table_name が 'employees' なら安全だが...
-- p_table_name が 'employees WHERE 1=1 UNION SELECT password FROM users--' なら危険
v_sql := 'SELECT COUNT(*) FROM ' || p_table_name;
EXECUTE IMMEDIATE v_sql INTO v_cnt;
DBMS_OUTPUT.PUT_LINE('件数: ' || v_cnt);
END;
/
-- 攻撃例:
-- EXEC unsafe_count('employees WHERE 1=1; DROP TABLE orders;--');
-- → DROP TABLE orders が実行される可能性がある
-- OK: DBMS_ASSERT でテーブル名を検証してから使う
CREATE OR REPLACE PROCEDURE safe_count(p_table_name VARCHAR2) AS
v_sql VARCHAR2(500);
v_cnt NUMBER;
v_safe_name VARCHAR2(100);
BEGIN
-- SQL_OBJECT_NAME: データベースに存在するオブジェクト名かどうかを検証する
-- 存在しない・不正な識別子の場合は例外(ORA-44002)が発生する
v_safe_name := DBMS_ASSERT.SQL_OBJECT_NAME(p_table_name);
v_sql := 'SELECT COUNT(*) FROM ' || v_safe_name;
EXECUTE IMMEDIATE v_sql INTO v_cnt;
DBMS_OUTPUT.PUT_LINE('件数: ' || v_cnt);
END;
/
ENQUOTE_NAME でオブジェクト名をダブルクォートで安全にエスケープする
ENQUOTE_NAME は識別子(テーブル名・列名など)をダブルクォート(”)で囲んで返します。識別子内に特殊文字が含まれていてもエスケープされます。オプションで大文字変換も行います。
ENQUOTE_NAME の使い方
DECLARE
v_result VARCHAR2(200);
BEGIN
-- 基本的な使い方: 識別子をダブルクォートで囲む
v_result := DBMS_ASSERT.ENQUOTE_NAME('employees');
DBMS_OUTPUT.PUT_LINE(v_result); -- → "EMPLOYEES"(大文字変換あり)
-- 第2引数 FALSE: 大文字変換しない
v_result := DBMS_ASSERT.ENQUOTE_NAME('employees', FALSE);
DBMS_OUTPUT.PUT_LINE(v_result); -- → "employees"(そのまま)
-- スペースを含む識別子もダブルクォートで安全に扱える
v_result := DBMS_ASSERT.ENQUOTE_NAME('my table', FALSE);
DBMS_OUTPUT.PUT_LINE(v_result); -- → "my table"
-- ダブルクォートが含まれる場合は "" にエスケープされる
v_result := DBMS_ASSERT.ENQUOTE_NAME('col"name', FALSE);
DBMS_OUTPUT.PUT_LINE(v_result); -- → "col""name"
END;
/
-- EXECUTE IMMEDIATE でのテーブル名の安全な使い方
DECLARE
v_table VARCHAR2(100) := 'employees'; -- 外部から受け取るテーブル名
v_sql VARCHAR2(500);
v_cnt NUMBER;
BEGIN
-- SQL_OBJECT_NAME で存在確認 + ENQUOTE_NAME でクォート
v_sql := 'SELECT COUNT(*) FROM '
|| DBMS_ASSERT.ENQUOTE_NAME(DBMS_ASSERT.SQL_OBJECT_NAME(v_table));
EXECUTE IMMEDIATE v_sql INTO v_cnt;
DBMS_OUTPUT.PUT_LINE(v_cnt);
END;
/
ENQUOTE_LITERAL で文字列リテラルをシングルクォートでエスケープする
ENQUOTE_LITERAL は文字列値をシングルクォートで囲み、文字列内のシングルクォートを '' (2つのシングルクォート)にエスケープします。WHERE 句の文字列条件を動的に組み立てるときに使います。
ENQUOTE_LITERAL の使い方
DECLARE
v_result VARCHAR2(200);
v_name VARCHAR2(100) := 'O''Brien'; -- シングルクォートを含む名前
v_sql VARCHAR2(500);
BEGIN
-- シングルクォートを含む文字列を安全にエスケープする
v_result := DBMS_ASSERT.ENQUOTE_LITERAL(v_name);
DBMS_OUTPUT.PUT_LINE(v_result);
-- → 'O''Brien' (シングルクォートが ''' にエスケープされた状態)
-- WHERE 句の文字列条件を安全に組み立てる
v_sql := 'SELECT * FROM employees WHERE last_name = '
|| DBMS_ASSERT.ENQUOTE_LITERAL(v_name);
DBMS_OUTPUT.PUT_LINE(v_sql);
-- → SELECT * FROM employees WHERE last_name = 'O''Brien'
-- NOOP: 何もしない(値をそのまま返す。ホワイトリスト済みの値に使う)
v_result := DBMS_ASSERT.NOOP('already_safe_value');
-- 処理を見やすくするために DBMS_ASSERT.NOOP() でラップしてレビューアに意図を示す
END;
/
SCHEMA_NAME / SQL_OBJECT_NAME / QUALIFIED_SQL_NAME で識別子を検証する
識別子検証関数の使い方と例外動作
DECLARE
v_schema VARCHAR2(100);
v_obj_name VARCHAR2(100);
v_qual VARCHAR2(200);
v_simple VARCHAR2(100);
BEGIN
-- SCHEMA_NAME: スキーマ(ユーザー)が存在するかを検証する
-- 存在しない場合: ORA-44001 (invalid schema name) が発生する
v_schema := DBMS_ASSERT.SCHEMA_NAME('HR');
DBMS_OUTPUT.PUT_LINE('スキーマ: ' || v_schema); -- → HR
-- SQL_OBJECT_NAME: データベースに存在するオブジェクト名(スキーマ修飾可)を検証する
-- 存在しない場合: ORA-44002 (invalid object name) が発生する
v_obj_name := DBMS_ASSERT.SQL_OBJECT_NAME('HR.EMPLOYEES');
DBMS_OUTPUT.PUT_LINE(v_obj_name); -- → HR.EMPLOYEES
-- QUALIFIED_SQL_NAME: スキーマ.オブジェクト の形式が正しいかを構文検証する
-- (オブジェクトが実際に存在するかは確認しない)
v_qual := DBMS_ASSERT.QUALIFIED_SQL_NAME('HR.EMPLOYEES');
DBMS_OUTPUT.PUT_LINE(v_qual); -- → HR.EMPLOYEES
-- SIMPLE_SQL_NAME: 単純な識別子構文(英数字・_・$・#)かどうかを検証する
-- 存在確認はしない。不正な文字が含まれていれば ORA-44003 が発生する
v_simple := DBMS_ASSERT.SIMPLE_SQL_NAME('EMPLOYEE_ID');
DBMS_OUTPUT.PUT_LINE(v_simple); -- → EMPLOYEE_ID
EXCEPTION
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('検証エラー: ' || SQLERRM);
-- ORA-44001: invalid schema name
-- ORA-44002: invalid object name
-- ORA-44003: invalid SQL name
-- ORA-44004: invalid qualified SQL name
END;
/
| 関数 | 検証内容 | 存在確認 | 主な用途 |
|---|---|---|---|
SIMPLE_SQL_NAME |
識別子として有効な文字のみかどうか | なし | 列名・変数名の構文チェック |
QUALIFIED_SQL_NAME |
schema.object 形式の構文が正しいか | なし | スキーマ修飾名の構文チェック |
SCHEMA_NAME |
スキーマが存在するか(DBA_USERS に存在) | あり | スキーマ名の有効性確認 |
SQL_OBJECT_NAME |
オブジェクトが存在するか(DBA_OBJECTS に存在) | あり | テーブル・ビュー・プロシージャ名の確認 |
ENQUOTE_NAME |
ダブルクォートで囲む | なし | 識別子をクォートして安全に使う |
ENQUOTE_LITERAL |
シングルクォートで囲む | なし | 文字列リテラルをエスケープする |
まとめ
- DBMS_ASSERT が必要な理由:動的 SQL でオブジェクト名にバインド変数が使えないため、文字列連結時の SQL インジェクション対策として必須
- SQL_OBJECT_NAME:テーブル名・ビュー名など動的 SQL のオブジェクト名検証に最もよく使う。存在しないオブジェクト名なら ORA-44002 が発生する
- ENQUOTE_NAME:検証済み識別子をダブルクォートで囲む。大文字変換オプションあり
- ENQUOTE_LITERAL:文字列リテラルをシングルクォートで囲む。シングルクォートを含む文字列も安全にエスケープできる
- SIMPLE_SQL_NAME:オブジェクトの存在確認なしに識別子の構文のみを検証する。列名など実行時にしか分からない名前の構文チェックに使う
- DBMS_ASSERT だけに頼らない:可能な限りホワイトリスト方式(許可するテーブル名を CASE 文で列挙)との組み合わせが推奨される
EXECUTE IMMEDIATE を使った動的 SQL の基本については EXECUTE IMMEDIATE 完全ガイドを参照してください。DBMS_SQL を使ったカーソルレベルの動的 SQL については DBMS_SQL 完全ガイドも参照してください。