PL/SQLの動的SQLは柔軟さと引き換えに、SQLインジェクションという重大なセキュリティリスクと隣り合わせです。「EXECUTE IMMEDIATE 'SELECT ... WHERE id=' || p_id」のような文字列連結方式は、攻撃者にとってはテーブル全件流出・全件削除・パスワード抽出への直通ルートになります。実際のOWASP Top 10でもインジェクションは長年トップ脅威として扱われており、古い実装が現役で本番にいるのが業界の現実です。
正解は「リテラルは必ずバインド変数」「識別子はDBMS_ASSERTとホワイトリスト」のたった2原則です。これさえ守れば、Oracleの動的SQLはむしろ静的SQLより安全な領域へ持ち込めます。さらに副次効果としてカーソル共有によるパース削減(同じSQLが何度実行されても1回だけパース)が効き、安全と性能が同時に手に入る構造になります。
この記事ではセキュリティに全振りした動的SQL実戦ガイドとして、攻撃の実例再現、EXECUTE IMMEDIATEでのUSING正しい書き方、識別子動的化の罠とDBMS_ASSERT実戦、RETURNING INTO・OUT・IN OUTの全パターン、ホワイトリスト設計、動的SQL監査の組み込み、アンチパターン7選、テスト方法、FAQ10問まで2026年版で整理します。
この記事でわかること
- SQLインジェクションが具体的にどんな攻撃文字列で成立するかの再現
- EXECUTE IMMEDIATEのUSING句で正しくバインドする実装パターン
- RETURNING INTO・OUT・IN OUTを使った戻り値取得の3パターン
- テーブル名・カラム名など識別子を動的化する場合のDBMS_ASSERT活用
- ホワイトリスト設計でDBMS_ASSERTだけでは防げない攻撃を遮断する方法
- 動的SQLとカーソル共有・パース削減の関係(性能と安全の両立)
- 動的SQL実行ログを取って異常な呼び出しを検知する監査設計
- 本番で踏むアンチパターン7選と修正パターン
- 動的SQLのセキュリティテスト方法(攻撃ペイロード集)
30秒でわかる動的SQLセキュア化の結論
忙しい読者向けの結論先出しです。
| 結論 | 理由・根拠 |
|---|---|
| ① リテラルは100%バインド変数(USING句)に置き換える | 文字列連結はSQLインジェクションの温床。例外なし |
| ② 識別子(表・列名)の動的化はDBMS_ASSERT+ホワイトリスト | DBMS_ASSERTだけでは「存在する有効名」を許してしまうので両輪必須 |
| ③ DDLはサブプログラム単位で許可制 | 動的DDLは特権昇格の入口。利用箇所をパッケージで集中管理 |
| ④ RETURNING INTO + USING OUTで戻り値を安全に取得 | 戻り値もバインドで受け取り、文字列連結で展開しない |
| ⑤ NDS(EXECUTE IMMEDIATE)優先、複雑時のみDBMS_SQL | NDSはバインド強制力が強く誤実装しにくい |
| ⑥ 動的SQL監査ログを仕込む | 異常な実行パターン(例: 1秒に100回違う表名)を後から検知できる |
| ⑦ セキュリティテストを必ず行う | 古典ペイロード10個で動的SQL関数を叩くテストを最低限実装 |
SQLインジェクションの実例|何が起きるかを知る
セキュアな書き方を学ぶ前に、悪い書き方が具体的にどう攻撃されるかを見ておくと実装ルールに対する納得感が変わります。以下は実際に攻撃が成立する典型例です。
シナリオ1|認証バイパス(一番ベタな攻撃)
ユーザーIDとパスワードを画面入力で受け取り、認証する古典実装を例に取ります。文字列連結方式だと、攻撃者は「' OR '1'='1」をパスワード欄に入れるだけで認証を素通りできます。
-- ❌ 脆弱:文字列連結でWHEREを組み立てている
CREATE OR REPLACE FUNCTION authenticate_bad(
p_user IN VARCHAR2,
p_pass IN VARCHAR2
) RETURN BOOLEAN AS
v_cnt NUMBER;
BEGIN
EXECUTE IMMEDIATE
'SELECT COUNT(*) FROM users
WHERE username = ''' || p_user || '''
AND password = ''' || p_pass || ''''
INTO v_cnt;
RETURN v_cnt > 0;
END;
/
-- 攻撃: パスワードに ' OR '1'='1 を入力されると...
-- 生成されるSQL:
-- SELECT COUNT(*) FROM users
-- WHERE username = 'alice' AND password = '' OR '1'='1
-- → 必ず1件以上ヒットして認証通過してしまう
シナリオ2|情報窃取(UNIONインジェクション)
商品検索のキーワード欄にx' UNION SELECT username,password FROM users--のような文字列を入れられると、結果セットにユーザーテーブルの中身が混入します。WebアプリがそのままJSONに変換して返してしまえば、画面でハッシュ化されていないパスワードが丸見えです。
シナリオ3|全件削除(DDL/DCLインジェクション)
並び順(ORDER BY column)を画面で選ばせる実装に対してid; DROP TABLE products--を渡されたら、動的SQLの種類によっては2文目が実行されてしまいます。EXECUTE IMMEDIATEは1文制約があるため助かるケースもありますが、DBMS_SQLや複文許容の構造では即死します。
「うちのコードはWHERE句だけ動的だから安全」は誤解です。WHERE句に攻撃文字列を入れるだけでUNIONによる任意テーブル参照・サブクエリでの全件抽出・条件分岐でのデータ推測(ブラインドSQLi)が成立します。「文字列連結している瞬間に脆弱」と認識を改めてください。
正しい実装|USING句でバインド変数を使い切る
動的SQLでセキュリティを担保する最も重要な機構がEXECUTE IMMEDIATE ... USING句です。プレースホルダ:1, :2または:p_user, :p_passをSQL文中に置き、実値はUSING句で別経路で渡します。SQL本文と値が別々の経路でDBに送られるため、攻撃者がいくら値に「’ OR ‘1’=’1」を入れても「文字列値そのもの」として扱われ、SQL文の一部にはなり得ません。
-- ✅ セキュア:USING句でバインド
CREATE OR REPLACE FUNCTION authenticate_safe(
p_user IN VARCHAR2,
p_pass IN VARCHAR2
) RETURN BOOLEAN AS
v_cnt NUMBER;
BEGIN
EXECUTE IMMEDIATE
'SELECT COUNT(*) FROM users
WHERE username = :u
AND password = :p'
INTO v_cnt
USING p_user, p_pass; -- ← 値はUSINGで渡す
RETURN v_cnt > 0;
END;
/
-- 攻撃: パスワードに ' OR '1'='1 を入力しても...
-- バインド値そのものとして扱われるため
-- WHERE password = (文字列値そのまま) になり常にFALSE
-- → 認証は安全に拒否される
USING句の3つの方向(IN/OUT/IN OUT)
USING句に渡す変数には3つの方向を指定できます。デフォルトはINで、戻り値を取りたい場合に明示的にOUTを指定します。INSERT直後の自動採番値を取得するなど、戻り値経路もバインドで安全に書ける設計が組まれています。
-- ① IN(デフォルト):値をSQLに渡す
EXECUTE IMMEDIATE
'UPDATE products SET price = :p WHERE id = :i'
USING 1500, 100;
-- ② OUT:SQLから値を受け取る(INSERT ... RETURNING や PL/SQL関数呼び出し)
DECLARE
v_id NUMBER;
BEGIN
EXECUTE IMMEDIATE
'INSERT INTO orders(order_id, customer_id, amount)
VALUES(orders_seq.NEXTVAL, :c, :a)
RETURNING order_id INTO :id'
USING 100, 5000, OUT v_id; -- OUT を明示
DBMS_OUTPUT.PUT_LINE('採番: ' || v_id);
END;
/
-- ③ IN OUT:値を渡しつつ更新後の値を受け取る
DECLARE
v_qty NUMBER := 10;
BEGIN
EXECUTE IMMEDIATE
'BEGIN
:q := :q * 2; -- 受け取って2倍にして返す
END;'
USING IN OUT v_qty;
DBMS_OUTPUT.PUT_LINE('更新後: ' || v_qty); -- 20
END;
/
-- ④ SELECT INTO 形式(単一行):INTO句 + USING句 で組み合わせ
DECLARE
v_name VARCHAR2(100);
BEGIN
EXECUTE IMMEDIATE
'SELECT product_name FROM products WHERE id = :i'
INTO v_name
USING 100;
END;
/
-- ⑤ BULK COLLECT INTO 形式:複数行をまとめて取得
DECLARE
TYPE t_names IS TABLE OF VARCHAR2(100);
v_names t_names;
BEGIN
EXECUTE IMMEDIATE
'SELECT product_name FROM products WHERE category = :c'
BULK COLLECT INTO v_names
USING 'BOOK';
END;
/
注意:USING句でNULLを渡したい場合はNULLリテラルを直接書けません。一旦変数にNULLを代入してからUSING v_nullで渡します。またVARCHAR2の長さ制限はOracleバインドでは32767バイトのため、CLOBは別途USING dbms_lob.substr(...)などで分割するか、CLOB変数で直接渡すかを使い分けます。
識別子の動的化|表・列名は別ルートで安全化する
「テーブル名や列名を動的に変えたい」という要件は実務でしばしば発生します。しかしUSING句は値(リテラル)にしか使えず、識別子には使えません。ここで安易に文字列連結すると一気に攻撃面が広がります。識別子の動的化にはDBMS_ASSERTとホワイトリストの二段ガードが必須です。
DBMS_ASSERTの主要関数
OracleはSQLインジェクション対策の標準パッケージとしてDBMS_ASSERTを提供しています。名前として有効な形式かを検査し、不正なら例外を投げてくれます。主な関数は次の4つです。
SIMPLE_SQL_NAME(s):単純SQL名(識別子)として有効か検査。ダブルクォート不要の英数字・アンダースコア・$#を許容QUALIFIED_SQL_NAME(s):schema.object@dblink形式の検査SCHEMA_NAME(s):実際に存在するスキーマ名か検証SQL_OBJECT_NAME(s):実際に存在するSQLオブジェクト名か検証ENQUOTE_NAME(s):ダブルクォートで安全に括るENQUOTE_LITERAL(s):シングルクォートで安全に括る
CREATE OR REPLACE PACKAGE pkg_safe_dyn AS
-- 許可テーブルの集合(ホワイトリスト)
TYPE t_allow IS TABLE OF VARCHAR2(30);
c_allowed_tabs CONSTANT t_allow := t_allow(
'PRODUCTS', 'ORDERS', 'CUSTOMERS', 'INVENTORY'
);
FUNCTION resolve_table(p_name VARCHAR2) RETURN VARCHAR2;
PROCEDURE truncate_safe(p_table VARCHAR2);
END pkg_safe_dyn;
/
CREATE OR REPLACE PACKAGE BODY pkg_safe_dyn AS
FUNCTION resolve_table(p_name VARCHAR2) RETURN VARCHAR2 AS
v_upper VARCHAR2(30);
BEGIN
-- ① DBMS_ASSERTで形式チェック(不正名は例外)
v_upper := UPPER(DBMS_ASSERT.SIMPLE_SQL_NAME(p_name));
-- ② ホワイトリスト判定(事前許可された名前か)
FOR i IN 1 .. c_allowed_tabs.COUNT LOOP
IF v_upper = c_allowed_tabs(i) THEN
RETURN v_upper; -- 安全な名前を返す
END IF;
END LOOP;
RAISE_APPLICATION_ERROR(-20001,
'許可されていないテーブル名: ' || p_name);
END;
PROCEDURE truncate_safe(p_table VARCHAR2) AS
v_safe VARCHAR2(30);
BEGIN
v_safe := resolve_table(p_table); -- 必ずホワイトリスト経由
EXECUTE IMMEDIATE 'TRUNCATE TABLE ' || v_safe;
END;
END pkg_safe_dyn;
/
-- 使い方
BEGIN
pkg_safe_dyn.truncate_safe('PRODUCTS'); -- OK
-- pkg_safe_dyn.truncate_safe('SYS.AUD$'); -- ORA-20001 で拒否
-- pkg_safe_dyn.truncate_safe('A--; DROP'); -- DBMS_ASSERTで例外
END;
/
DBMS_ASSERT単独では不十分な点に注意してください。SIMPLE_SQL_NAMEは「形式として有効な識別子か」しか見ないので、存在するテーブル名であればSYS.USER$でもHR.SALARYでも通過してしまいます。アプリが本来触ってはいけない表へのアクセスを防ぐにはホワイトリストとの併用が必須です。
DDLを動的に実行する場合の注意点
DDL(CREATE/ALTER/DROP/TRUNCATE)は構造上バインド変数を使えないため、識別子を文字列連結する以外に実装手段がありません。すなわち動的DDLは攻撃面の塊であり、次の3原則を必ず守ってください。
- 呼び出し権限を絞る:動的DDLを実行するパッケージに付与する権限はDBA権限ではなく必要最小限へ。Oracle 12c以降は
AUTHID CURRENT_USER+INHERIT PRIVILEGES制御で「呼び出し元の権限で動かす」設計が安全 - 受け取る引数は完全制限:DBMS_ASSERT+ホワイトリストで「許可された名前のみ」「許可された操作のみ」に絞り、自由文字列の受け取りを禁止
- 監査ログを必ず残す:誰がいつ何のDDLを実行したかを記録し、想定外の呼び出しを後追いできるようにする
CREATE OR REPLACE PACKAGE pkg_admin_dyn AUTHID CURRENT_USER AS
-- 許可される操作のみ列挙
TYPE t_op IS TABLE OF VARCHAR2(20);
c_ops CONSTANT t_op := t_op('TRUNCATE', 'REBUILD');
PROCEDURE do_ddl(p_op IN VARCHAR2, p_target IN VARCHAR2);
END;
/
CREATE OR REPLACE PACKAGE BODY pkg_admin_dyn AS
PROCEDURE do_ddl(p_op IN VARCHAR2, p_target IN VARCHAR2) AS
v_op VARCHAR2(20);
v_tgt VARCHAR2(30);
v_sql VARCHAR2(500);
v_ok BOOLEAN := FALSE;
BEGIN
-- 操作のホワイトリスト
v_op := UPPER(DBMS_ASSERT.SIMPLE_SQL_NAME(p_op));
FOR i IN 1 .. c_ops.COUNT LOOP
IF v_op = c_ops(i) THEN v_ok := TRUE; EXIT; END IF;
END LOOP;
IF NOT v_ok THEN
RAISE_APPLICATION_ERROR(-20002, '許可されない操作: ' || p_op);
END IF;
-- 対象のホワイトリスト(pkg_safe_dyn を再利用)
v_tgt := pkg_safe_dyn.resolve_table(p_target);
-- 監査ログ
INSERT INTO dyn_audit(op, target, who, at_ts)
VALUES(v_op, v_tgt, USER, SYSTIMESTAMP);
-- 実行
CASE v_op
WHEN 'TRUNCATE' THEN
EXECUTE IMMEDIATE 'TRUNCATE TABLE ' || v_tgt;
WHEN 'REBUILD' THEN
EXECUTE IMMEDIATE 'ALTER INDEX IDX_' || v_tgt || ' REBUILD';
END CASE;
END;
END;
/
動的SQL監査|異常な実行を後から検知する
セキュア実装でも「権限を持つ正規ユーザによる悪意ある操作」までは完全には防げません。事後検知のために動的SQL実行ログを必ず残します。Oracle標準のUnified Auditingと独自の動的SQLラッパーを組み合わせるのが実務的なベストプラクティスです。
-- 監査表
CREATE TABLE dyn_sql_log(
log_id NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
exec_at TIMESTAMP DEFAULT SYSTIMESTAMP,
who VARCHAR2(64) DEFAULT USER,
app_id VARCHAR2(64), -- アプリ識別子(CONTEXTで埋める)
category VARCHAR2(20), -- DML/DDL/QUERY
sql_text CLOB,
bind_args CLOB, -- JSONで構造化
err_code NUMBER,
err_msg VARCHAR2(4000)
);
-- ラッパー
CREATE OR REPLACE PROCEDURE exec_safe(
p_sql IN VARCHAR2,
p_category IN VARCHAR2,
p_args IN CLOB DEFAULT NULL -- JSONで渡す
) AS
PRAGMA AUTONOMOUS_TRANSACTION;
v_log_id NUMBER;
BEGIN
-- 実行前に監査ログ
INSERT INTO dyn_sql_log(category, sql_text, bind_args)
VALUES(p_category, p_sql, p_args)
RETURNING log_id INTO v_log_id;
COMMIT;
-- 実行は呼び出し元のコンテキストで(ここでは型省略の例示)
EXECUTE IMMEDIATE p_sql;
EXCEPTION
WHEN OTHERS THEN
UPDATE dyn_sql_log
SET err_code = SQLCODE,
err_msg = SUBSTR(SQLERRM,1,4000)
WHERE log_id = v_log_id;
COMMIT;
RAISE;
END;
/
異常検知のサンプルクエリ
監査ログが溜まれば次のような検知クエリが書けます。
-- ① 1分間に同一ユーザが100回以上動的SQLを叩いている
SELECT who, COUNT(*) cnt
FROM dyn_sql_log
WHERE exec_at > SYSTIMESTAMP - INTERVAL '1' MINUTE
GROUP BY who
HAVING COUNT(*) >= 100
ORDER BY cnt DESC;
-- ② 危険キーワードを含むSQL(既に防いでいるはずだが二重チェック)
SELECT log_id, who, sql_text
FROM dyn_sql_log
WHERE REGEXP_LIKE(sql_text, '(UNION|--|;\s*DROP|;\s*DELETE|sys\.)', 'i')
AND exec_at > SYSDATE - 1;
-- ③ 通常使われない表へのアクセス(ホワイトリスト外)
SELECT log_id, who, sql_text
FROM dyn_sql_log
WHERE NOT REGEXP_LIKE(sql_text,
'PRODUCTS|ORDERS|CUSTOMERS|INVENTORY', 'i')
AND category IN ('DML','DDL')
AND exec_at > SYSDATE - 1;
Oracle 12c以降のUnified Auditingをパッケージ単位で有効化(CREATE AUDIT POLICY)すれば、カスタム監査ログを書かなくても標準のUNIFIED_AUDIT_TRAILビューで実行履歴を追跡できます。Auditingオプションが利用可能なライセンスならこちらを優先し、カスタムログは「アプリ固有のIDやユーザコンテキストを残したい場合」に補完で使います。
セキュリティと性能の両立|カーソル共有の効果
バインド変数を使うとセキュリティが上がるだけでなく、副次効果としてカーソル共有が効きます。同じSQL文(バインドプレースホルダが揃った状態)が何度実行されてもパースは1回だけで済み、ライブラリキャッシュ消費もリテラル展開版の数十分の1〜数百分の1になります。
パース回数の差の実測イメージ
同一処理を100万回実行した場合、リテラル連結版は100万通りのSQLとして100万回パースされライブラリキャッシュを爆発させます。バインド版は1回パースして以降はソフトパースまたはキャッシュヒットで応答するため、CPU使用率が桁違いに低くなります。
カーソル共有が効く条件
- SQL本文の1文字も違わない(空白の数も含む)
- バインド変数が同じ位置に同じ型・近い長さで登場
CURSOR_SHARING=EXACTがデフォルト。FORCEはリテラルを自動でバインド化する裏技だが副作用大- 大文字小文字の違いも別SQLとして扱われるのでSQL本文は決め打ちで定数化するのがベスト
本番で踏むアンチパターン7選
① WHERE値を文字列連結する
最も古典的かつ最も致命的な脆弱性。たとえ「数値しか来ない」と分かっていても必ずバインド変数に統一してください。将来、別の開発者が同じ関数を別用途で使った瞬間に脆弱性が顕在化します。
② DBMS_ASSERTで満足してホワイトリストを省略
DBMS_ASSERTは形式チェックしかしないので、SYS.USER$のような有効名を阻止できません。識別子の動的化には必ずホワイトリストを併用してください。
③ ORDER BY句に画面入力をそのまま入れる
並び順カラム名を可変にする実装でORDER BY <ユーザ入力>と書くと、入力値にCASE WHEN (SELECT...) THEN 1 ELSE 1/0 ENDを仕込まれてブラインドSQLiが成立します(条件が真なら除算エラーで応答が変わる、という反応差からパスワードを1文字ずつ抽出される攻撃)。ORDER BY列は必ずホワイトリスト化、または列番号:nでバインドしてください。
④ DDLにDEFINER権限の固定パッケージを使う
DDLパッケージをAUTHID DEFINER(デフォルト)で書くと、パッケージ所有者の強い権限で動いてしまい権限昇格の入口になります。DDL実行系はAUTHID CURRENT_USER+必要最小権限で運用するのが鉄則です。
⑤ 例外を握りつぶしてエラー情報を返さない
「EXCEPTION WHEN OTHERS THEN NULL;」で例外を消すと、インジェクション試行を検知できないだけでなく攻撃者にとっては反応の差で情報を抽出するブラインドSQLiの土壌になります。例外は必ずログ→再raiseしてください。
⑥ 攻撃テストをしないままリリース
古典ペイロード10個(' OR '1'='1・'; DROP TABLE x--・' UNION SELECT ... --等)を関数に投入して例外or拒否されることを確認するだけのテストでも実装の8割の脆弱性は炙り出せます。リリース前のチェックリストに必ず入れてください。
⑦ 動的SQL監査を取らない
「セキュアに書いたから安心」と監査を省くと、事後の異常検知や法令対応で詰みます。Unified Auditingまたは独自ログで誰が何を動的に実行したかを必ず保管してください。
動的SQLのセキュリティテスト|古典ペイロード10種
セキュア実装の確認のために、関数を直接攻撃ペイロードで叩く単体テストを仕込みます。次の10種をすべて投入し、いずれも例外or業務的拒否で結果が返ることを検証してください。
DECLARE
TYPE t_payloads IS TABLE OF VARCHAR2(200);
v_payloads t_payloads := t_payloads(
Q'[' OR '1'='1]', -- 1: 認証バイパス
Q'[admin'--]', -- 2: コメント切り
Q'['; DROP TABLE products--]', -- 3: DDL注入
Q'[' UNION SELECT username,password FROM users--]', -- 4: UNION窃取
Q'[1; DELETE FROM orders WHERE 1=1--]', -- 5: DML注入
Q'[' OR (SELECT COUNT(*) FROM users)>0--]', -- 6: ブラインド
Q'[%' OR password LIKE '%a%' OR ']', -- 7: LIKEワイルド
Q'[admin' AND SYS_CONTEXT('USERENV','SID')>0--]', -- 8: 関数経由
Q'[' || (SELECT MAX(id) FROM users) || ']', -- 9: 連結試行
Q'[NULL OR 1=1--]' -- 10: NULL含み
);
v_ok PLS_INTEGER := 0;
v_ng PLS_INTEGER := 0;
BEGIN
FOR i IN 1 .. v_payloads.COUNT LOOP
BEGIN
-- 認証関数(authenticate_safe)が攻撃で通ってしまわないことを確認
IF authenticate_safe('alice', v_payloads(i)) THEN
v_ng := v_ng + 1;
DBMS_OUTPUT.PUT_LINE('NG: payload #' || i || ' bypassed!');
ELSE
v_ok := v_ok + 1;
END IF;
EXCEPTION
WHEN OTHERS THEN
v_ok := v_ok + 1; -- 例外も「拒否」として正常
END;
END LOOP;
DBMS_OUTPUT.PUT_LINE('OK=' || v_ok || ' / NG=' || v_ng);
IF v_ng > 0 THEN
RAISE_APPLICATION_ERROR(-20003, 'セキュリティテスト失敗');
END IF;
END;
/
よくある質問
%と_をESCAPE文字でエスケープしてください。v_kw := REPLACE(REPLACE(p_kw,'\\','\\\\'),'%','\\%')のように前処理し、SQL側はLIKE :kw ESCAPE '\\'と書きます。これでユーザーが「%」を含む検索語を入れても部分一致が暴発せず、かつバインドの安全性は維持されます。:1, :2, ..., :10を全部宣言し未使用はNULLバインド、②値をコンマ連結文字列にしてWHERE x IN (SELECT REGEXP_SUBSTR(...) FROM dual CONNECT BY ...)のように展開、③コレクション型を作ってWHERE x MEMBER OF :collでバインド。③が最も柔軟で安全です。CREATE OR REPLACE SYNONYMで切り替えるのが安全です。これでSQL本文に動的部分がなくなりインジェクション面が消滅し、カーソル共有も最大化されます。本番環境ではシノニム切替権限自体を厳しく制限してください。DBMS_XPLAN.DISPLAY_CURSOR(format=>'+PEEKED_BINDS')で実際にピークされた値を確認できます。USING l_clobでバインド可能です。長大なテキスト(JSON・CSV等)を扱う場合に使います。BLOBも同様でUSING l_blob。注意点はカーソル共有を考慮するなら同じ場所に同じ型のLOBを渡し続ける設計にすること。型が変動するとハードパースが頻発し性能が劣化します。関連記事で知識を深める
動的SQLとセキュリティ・性能設計に関連する記事を集めました。
- 【Oracle】EXECUTE IMMEDIATE(動的SQL)完全ガイド|DDL実行・USING・RETURNING INTO・BULK COLLECT・DBMS_SQL比較・SQLインジェクション対策まで実例で解説(機能網羅と基礎構文)
- 【PL/SQL】動的SQLとBIND変数の最適設計:SQL Injection防止とキャッシュ効率の両立(カーソル共有とFORALL一括バインドの応用)
- 【Oracle】DBMS_SQL完全ガイド|動的SQLのカーソル操作・列数不定のSELECT・EXECUTE IMMEDIATE との使い分け(DBMS_SQLの詳細)
- 【Oracle】バインド変数完全ガイド(バインドの基礎とExplain Plan活用)
- 【PL/SQL】コンパイル時エラーと警告の完全対処ガイド(USING句の誤用をPLSQL_WARNINGSで検出)
- 【PL/SQL】例外処理の書き方と使い方(動的SQL例外を握りつぶさない設計)
- 【PL/SQL】ストアドプロシージャとファンクションの違いと作り方(動的SQLラッパーの設計基礎)
- 【PL/SQL】MERGE文でUPSERTを高速・安全に実装(バインド付きMERGEの実装)
- 【PL/SQL】パイプライン関数で大量データ処理を勝たせる完全ガイド(動的SQL+パイプラインの組み合わせ)
- 【PL/SQL】DBMS_SCHEDULERでジョブ管理を極める(ジョブ内動的DDLの安全運用)
まとめ|セキュアな動的SQLは2原則で守れる
動的SQLのセキュリティは複雑そうに見えますが、本質は「リテラルはバインド」「識別子はDBMS_ASSERT+ホワイトリスト」の二段構造に集約されます。これを全動的SQL関数で例外なく適用し、加えてDDL実行ラッパー化・監査ログ・セキュリティテストの3点を運用に組み込めば、PL/SQLにおけるインジェクションは構造的に根絶できます。本記事の要点を7項目で再確認します。
- EXECUTE IMMEDIATEのUSING句で値は100%バインド変数に
- RETURNING INTO + USING OUTで戻り値もバインドで安全取得
- 識別子の動的化はDBMS_ASSERT+ホワイトリストの二段ガード
- DDLはAUTHID CURRENT_USER+操作・対象の二重ホワイトリスト
- 動的SQL実行をラッパーに集約しUnified Auditingまたは独自監査ログを残す
- カーソル共有でパース削減のおまけが付く(性能と安全の両立)
- 古典攻撃ペイロード10種を入れた単体テストをリリース条件に組み込む
レガシーコードの動的SQLを順次セキュア化していくときは、まずWHERE値の文字列連結を片端からUSING句に置換、次に識別子の動的化がある関数だけホワイトリスト経由に統一、最後にDDL実行系をラッパー集約、の3ステップで進めるとリスクの大きい順から潰せます。本記事を実装テンプレとして自プロジェクトの動的SQLを総点検してみてください。

