1. はじめに
最近は自律的に動く AI エージェントを攻撃者が悪用する想定も現実味を帯びてきており、攻撃者がアプリケーションサーバーに入り込んで、アプリが使っている DB 接続情報をそのまま使って任意の SQL を DB に投げる、というようなリスクも出てきました。
完全に防ぐのは難しいとしても、被害の範囲は絞りたいところです。たとえばアプリ用のユーザーが乗っ取られても SELECT * で機微なテーブルを丸ごと抜かれるのは止めたい、ADMIN のような強い権限はそもそもアプリケーションサーバー側に置かない、といった発想です。
この用途に合いそうなのが、Oracle Database 26ai の SQL Firewall です。公式ドキュメントには、DB のカーネルに組み込まれて "inspects all incoming database connections and SQL statements"(すべての受信接続と SQL 文を検査する)とあります1。あらかじめ許可した SQL と接続だけを通し、それ以外を検知・ブロックする仕組みです。
本記事では、ADB(26ai)で SQL Firewall を有効にし、アプリ用ユーザー APPUSER の正常な SQL を学習して許可リストにし、強制した状態で、普段アプリが投げない SQL や別経路からの接続が実際にどう止まるか、そして何が止まらないかを実機で確認します。
今回の検証ゴール
| # | 検証項目 |
|---|---|
| 1 | SQL 文の許可リストで、アプリが普段投げない SQL をブロックできるか |
| 2 | 接続コンテキスト(接続元 IP / OS ユーザー / プログラム)の許可リストで、想定外の接続元をブロックできるか |
| 3 | SQL Firewall で防げない範囲はどこか |
結論(先出し)
- 学習させた 2 本の業務クエリ以外(
SELECT *・データ辞書の探索・UNIONの継ぎ足し・GRANTなどの管理系の文)は、署名が許可リストに無いとしてブロックされた - 学習時と違うプログラムからの接続は、SQL を実行する前の接続時点でブロックされた
- 一方で、許可済みクエリの bind 値だけを変えて順に試す操作は通ってしまう。SQL Firewall は SQL の構造を見る仕組みであって、値レベルの持ち出しは止めない
- したがって完全防御ではなく、被害範囲(blast radius)を絞る仕組みと捉えるのが妥当だ
2. 検証環境
| 項目 | 内容 |
|---|---|
| 対象 DB | Oracle Autonomous Database 26ai(OLTP / 2 ECPU) |
| 管理経路 | Oracle Data Safe の SQL Firewall |
| クライアント | Windows 11 Pro + SQLcl / python-oracledb 4.0.1 |
| アプリ用ユーザー |
APPUSER(業務表 CUSTOMERS / ORDERS を所有) |
APPUSER には業務表を自分のスキーマに持たせています。こうすると、テーブルへのアクセス権(オブジェクト権限)では止まらず、SQL Firewall の許可リストだけが制御要因になります。「最小権限で見えないからアクセスできないのか」「SQL Firewall が止めたのか」を混ぜずに観察するための構成です。
攻撃者役は、乗っ取られたアプリケーションサーバーに見立てて、APPUSER と同じ接続情報を使って、普段アプリが投げない SQL を手で流します。コンテキスト違反の確認だけは、別のプログラム(python-oracledb)から接続して再現します。
ADB では SQL Firewall を Data Safe から管理します。Oracle Database の DBMS_SQL_FIREWALL パッケージを直接呼ぶ運用は、SYS 所有のメタデータに触れるため ADB では正規の経路ではありません2。本記事の操作はすべて Data Safe(および OCI CLI)から行います。
3. SQL Firewall の考え方
SQL Firewall の許可リストは、次の 2 つの軸を持ちます。
- 許可する SQL 文の署名(bind 変数を除いた、正規化した SQL の形)
- 許可する 接続コンテキスト(接続元 IP アドレス、OS ユーザー、プログラム名)3
強制のスコープはこの片方だけにも、両方(ENFORCE_ALL)にもできます。今回は両方を有効にします。今回の脅威モデルでの位置づけを図にすると次のようになります。
止められるのは、主に SQL の形が普段と変わるものです。普段アプリが投げない形の SQL、辞書を舐める偵察、SQL インジェクションのように文の構造が変わるもの。これらは署名が許可リストと一致しないのでブロックできます。接続元については、接続元 IP で絞る部分はネットワークファイアウォールと役割が重なります。SQL Firewall はそれに加えて OS ユーザーやプログラム名でも接続を絞れるので、同じホスト上でも許可外のプログラムからの接続を止められます。
一方で、許可済みの SQL の bind 値を変えるだけの操作は、署名が同じなので通ります。どこまで止まってどこから止まらないか、これが本記事で確かめたい点です。
4. Data Safe で許可リストを作る
Data Safe で SQL Firewall を有効化し、収集からポリシー生成・ブロックまでの画面ごとの手順は、orakurara さんの記事が詳しくまとまっています4。本記事は手順を要点に絞り、乗っ取り時に何が止まって何が止まらないかに紙面を割きます。以下は ADB ならではの詰まりどころの補足です。
4.1. 前提の有効化
SQL Firewall を Data Safe で使うには、先に 2 つの下ごしらえが要ります。どちらも実際に詰まった箇所です。
1 つ目は、Data Safe のサービスアカウントへの SQL Firewall ロールの付与です。ADB では大半のロールが既定で付与済みですが、DS$SQL_FIREWALL_ROLE だけは既定で付きません5。ADMIN で次を実行します。
EXECUTE DS_TARGET_UTIL.GRANT_ROLE('DS$SQL_FIREWALL_ROLE');
2 つ目は、DB 側で SQL Firewall 自体を有効化することです。これをせずに収集を作ろうとすると、Firewall is not enabled on the target database で失敗します。OCI CLI(ADMIN 権限のプロファイル)で、構成をリフレッシュしてから有効化します。
# ロール付与を反映(NEEDS_ATTENTION のまま残ることがあるため)
oci data-safe database-security-config refresh --database-security-config-id <OCID>
# DB の SQL Firewall を有効化
oci data-safe database-security-config update \
--database-security-config-id <OCID> \
--sql-firewall-config '{"status":"ENABLED"}' --force
4.2. 正常な SQL を学習する
APPUSER の SQL を集める「SQL 収集」を作成し、収集中にアプリの定型クエリを流します。今回アプリの正常な振る舞いとして流したのは次の 2 本です。
SELECT id, name FROM customers WHERE id = :1;
SELECT o.id, o.amount FROM orders o WHERE o.customer_id = :1;
bind 値を変えながら数回ずつ実行し、収集を止めます。
4.3. 許可リストを生成する
収集したログから許可リスト(SQL Firewall ポリシー)を生成します。生成された許可リストには、SQL 署名と接続コンテキストの両方が入ります。接続コンテキストは、収集を実行した端末の接続元 IP、OS ユーザー、プログラム名(今回は SQLcl)でした。
SQL 署名は 8 件になりました。意図した業務クエリ 2 本のほかに、6 件が混ざっています。
この 6 件は、SQLcl が接続時に自動で発行する初期化系の SQL です。ALTER SESSION SET TIME_ZONE、SELECT DBTIMEZONE FROM DUAL、NLS パラメータの取得、exec を包む PL/SQL のラッパなどです。業務クエリを 2 本しか流していないのに許可リストが 8 件になったのは、これが理由でした。
ここは正直に書いておきます。対話型のツールで学習すると、そのツール自身の初期化 SQL まで許可リストに入ります。本番では実際のアプリケーションで学習し、収集した内容を必ず目視でレビューするのが前提だと分かりました。今回はデモの見通しを良くするため、この 6 件は残したまま先へ進めます(攻撃 SQL だけが違反として立つので対比が読みやすくなります)。
5. 強制を有効にする
いきなりブロックにすると、学習漏れがあったときに正常なアプリを止めてしまいます。まずは監視のみ(違反ログは残すがブロックしない)で有効化し、正常クエリを流して誤検知が出ないことを確認しました。業務 2 本と初期化 6 件は許可済みのため、新しい違反は出ませんでした。
そのうえで、違反時の動作をブロックに切り替えます。最終状態は「有効・スコープは SQL と接続コンテキストの両方・違反はブロックしてログ」です。
違反時の動作は、この許可リスト単位で「監視のみ」か「ブロック」の二択でした。SQL ごとに重要度を見て「これは止める、これはログだけ」と出し分ける設定は見当たりません。明らかに危険なものだけブロックして残りはログに回す、といった段階的な扱いにしたいときは、監視のみで全違反をログに残し、重要度の判定や通知は統合監査やアラートなど別の仕組みに持たせる形になります。
今回はブロックまで有効にしましたが、本番では判断が分かれます。ブロックは学習漏れがあると正規のアプリを止めてしまうため、可用性を優先してあえて検知のみ(監視)で運用し、違反はアラートで拾って対処する、という進め方もあります。ブロックの強さと止めてしまうリスクは、対象システムの状況に応じて決める形になります。
6. 攻撃してみる
強制を有効にした状態で、APPUSER の接続情報を使って攻撃的な SQL を流します。#1 から #5 と #7 は、学習時と同じ端末(許可された接続コンテキスト)から SQLcl で実行しました。#6 だけは別プログラムから接続します。
| # | 実行した操作 | 結果 |
|---|---|---|
| 1 | SELECT id, name FROM customers WHERE id = :1 |
許可(3 名分を取得) |
| 2 | SELECT * FROM customers |
ブロック(ORA-47605) |
| 3 | SELECT table_name FROM all_tables WHERE owner = 'APPUSER' |
ブロック(ORA-47605) |
| 4 | 定型クエリに UNION SELECT ... を継ぎ足した改変 |
ブロック(下記の注記あり) |
| 5 |
GRANT DBA TO appuser / ALTER USER appuser QUOTA UNLIMITED ON DATA
|
ブロック(ORA-47605) |
| 6 | 別プログラムから接続して定型クエリ | ブロック(接続時点で ORA-47605) |
| 7 | 定型クエリと同じ署名のまま id を 1, 2, 3 と順に試す |
すべて許可 |
ORA-47605 は SQL Firewall による違反を示すエラーです。攻撃者から見ると、探索や UNION の継ぎ足し、GRANT などの管理系の文はこのエラーでブロックされます。
#4 の UNION の継ぎ足しには観察ポイントがありました。列の型が合わない版(数値列と文字列列を UNION)を投げると、クライアントには型不一致の ORA-01790 が返りますが、SQL Firewall の違反ログにも記録されていました。型を合わせた版では素直に ORA-47605 でブロックされます。型チェックと SQL Firewall の判定の順序差が見える例です。
#5 の GRANT DBA は少し性質が違います。そもそも APPUSER には他ユーザーへ DBA を付与する権限が無いので、SQL Firewall が無くても権限不足のエラーで失敗します。SQL Firewall はこれを権限チェックより前の SQL の段でブロックし、試み自体を違反ログに残しました。つまりここでの効果は「成功する昇格を止めた」ではなく「実行される前に止めて記録した」ことです。実際、後から ADMIN で確認しても APPUSER にはロールが 1 件も付いておらず、システム権限も CREATE SESSION と CREATE TABLE だけでした。
6.1. 別プログラムからの接続(コンテキスト違反)
#6 は、python-oracledb からプログラム名を attacker_tool として APPUSER で接続しました。許可リストのプログラム名は SQLcl なので一致しません。結果は、接続を確立する時点で ORA-47605 になり、拒否されました。許可済みの業務クエリを実行するところまで到達しません。mTLS のハンドシェイクは成立し、ログオンの段で接続コンテキストが評価されてブロックされる、という順序です。
6.2. 違反ログの確認
違反は 2 つの経路で確認できます。DB 側の DBA_SQL_FIREWALL_VIOLATIONS ビュー(ADMIN)と、Data Safe の違反レポートです。両者は同じ 14 件(SQL 違反 13 件、コンテキスト違反 1 件)を示し、内容も一致しました。
SQL 違反のレポートには、SELECT * FROM CUSTOMERS、ALL_TABLES の探索、GRANT DBA TO APPUSER などが並びます。
そして #6 のコンテキスト違反は、CAUSE が Context violation、操作が CONNECT、SQL テキストは空、クライアントプログラムが attacker_tool として、SQL 違反とは別に記録されていました。SQL ではなく接続コンテキストでブロックされたことが、この 1 行から読み取れます。
6.3. 防げない範囲(#7)
#7 は、許可済みの SELECT id, name FROM customers WHERE id = :1 を、id の値だけ 1, 2, 3 と変えて実行したものです。署名は同じなので、すべて許可されて全件が返りました。違反ログにも残りません。
攻撃者がアプリの正規クエリの形を保ったまま、bind 値を順に変えて顧客情報を 1 行ずつ抜く動きは、SQL Firewall では止まりません。ここが「被害範囲を絞る」であって「防ぎ切る」ではない、という違いです。
7. 考察
7.1. 止められた範囲
今回の観測では、SQL Firewall は次の 2 種類を止めました。1 つは SQL の形が普段と違うもの(SELECT *、辞書探索、UNION の継ぎ足し、GRANT などの管理系の文)で、これは署名の不一致としてブロックされます。もう 1 つは接続元が普段と違うもので、許可外のプログラムから接続すると接続時点でブロックされます。乗っ取られたアプリ用ユーザーが「普段と違うこと」をやろうとした瞬間に止まる、という挙動です。
7.2. 防げない範囲
一方、許可済みクエリの bind 値を変える持ち出しは通ります。SQL Firewall は SQL の構造と接続コンテキストを見る仕組みで、値レベルの濫用は対象外だからです。アプリの正規クエリが WHERE customer_id = :1 の形で 1 顧客ずつ引ける設計なら、攻撃者はその形のまま全顧客を舐められます。SQL Firewall はこの動きを異常とは見なしません。
7.3. SQL Firewall だけでは足りない
7.2 のとおり、SQL Firewall は許可済みクエリの bind 値までは見ません。アプリの正規クエリが 1 顧客ずつ引ける形なら、乗っ取った攻撃者はその形のまま顧客を順に引けます。取り出せる範囲はアプリ用ユーザーが本来アクセスできるデータと重なり、そこは狭まりません。
ここを補うには、SQL Firewall とは別の層を重ねる形になると考えられます。Oracle が機能として持つものを、担当で並べると次のようになります6(併用自体は今回の検証範囲外です)。
| 層 | 何をするか | この層が止めるもの |
|---|---|---|
| 最小権限(オブジェクト権限) | アプリ用ユーザーが触れる表を必要な範囲に絞る | 別テーブルへのアクセス(権限エラーで停止) |
| VPD / 行レベル・セキュリティ | 許可済みクエリでも返る行を担当分だけに限定する | 担当外の行の持ち出し(返る行を減らす) |
| Data Redaction |
email のような機微列をマスクする |
機微な値の読み取り(マスクで隠す) |
ただし乗っ取りでは、アプリ用ユーザーの身元ごと奪われます。そのため「特定ユーザーだけ非マスク」「アプリが設定する担当で行を絞る」といったユーザーやコンテキストに依存する設計は、攻撃者がその身元を名乗れるため、そのまま制御をすり抜けられます。実際に絞るには、全員一律のマスクや、接続ユーザーで固定した行の絞り込みのように、詐称できない形で一律に絞る形にします。
SQL Firewall はこれらの外側で「SQL の構造と接続元」を見る層です。最小権限は触れる対象、VPD や Data Redaction は行と列の値を担当し、層ごとに守る場所が違います。アプリ用ユーザーが乗っ取られる前提に立つと、どれか 1 つで守り切るより、層を重ねて被害を小さくする組み立てが要ります。
7.4. コンテキストの前提
今回のコンテキスト許可リストは、学習した端末(特定の IP・OS ユーザー・プログラム SQLcl)に固定されました。これはデモとしては分かりやすい一方、実際のアプリがコネクションプールを複数ホストで持つ、プログラム名が一定でない、といった環境では、許可コンテキストの設計を実運用に合わせて見直す必要があります。狭すぎれば正規のアプリをブロックし、広すぎればコンテキスト制御の意味が薄れます。
8. まとめ
| # | 検証項目 | 結論 |
|---|---|---|
| 1 | SQL 文の許可リストで普段投げない SQL を止められるか | 止められた。SELECT *・辞書探索・UNION の継ぎ足し・GRANT などを ORA-47605 でブロック |
| 2 | 接続コンテキストで想定外の接続元を止められるか | 止められた。別プログラムからの接続は SQL 実行前の接続時点でブロック |
| 3 | 防げない範囲はどこか | 許可済み SQL の bind 値を変える持ち出しは通る。構造は見るが値は見ない |
SQL Firewall は、アプリ用ユーザーが乗っ取られたときに「普段と違う SQL・普段と違う接続」を止める、被害範囲を絞るためのガードレールとして働きました。完全防御ではないので、値レベルの持ち出しには最小権限や行レベルの制御を併用する前提になります。ADMIN のような強い権限をアプリケーションサーバー側に置かない、という設計と組み合わせて、乗っ取り時の被害を一段小さくする現実的な一手だと感じました。
参考
-
Oracle SQL Firewall User's Guide (26ai). https://docs.oracle.com/en/database/oracle/oracle-database/26/sqlfw/oracle-sql-firewall.html ↩
-
Oracle Autonomous AI Database Serverless, Security Features Overview(SQL Firewall は Data Safe で管理). https://docs.oracle.com/en/cloud/paas/autonomous-database/serverless/adbsb/security-features-overview.html ↩
-
DBMS_SQL_FIREWALL(enforce 定数・接続コンテキスト属性). https://docs.oracle.com/en/database/oracle/oracle-database/26/arpls/dbms_sql_firewall.html ↩
-
orakurara「Oracle Database 23ai の SQL Firewall を Oracle Data Safe から設定してみた」(Data Safe での設定・ブロックの画面手順). https://qiita.com/orakurara/items/87b5c24ff3cec31aae49 ↩
-
Oracle Data Safe, Grant Roles to the Oracle Data Safe Service Account(
DS_TARGET_UTIL.GRANT_ROLE). https://docs.oracle.com/en/cloud/paas/data-safe/admds/grant-roles-oracle-data-safe-service-account-your-target-database.html ↩ -
Oracle Data Redaction Guide 26ai, "Oracle Data Redaction and Oracle Virtual Private Database"(VPD と Data Redaction の位置づけ). https://docs.oracle.com/en/database/oracle/oracle-database/26/dbred/oracle-data-redaction-and-oracle-virtual-private-database.html ↩







