検証用パーサと実行系パーサがずれると、検証は素通りする — pgAdmin 4 の CVE-2026-17351

「アプリ側で入力を検証してから、データベースへ渡す」。よくある設計だ。この設計が成立するには条件がひとつある。検証に使うパーサと、実際に実行する側のパーサが、まったく同じ解釈をすることである。

条件が崩れるとどうなるか。実例が出た。PostgreSQL の管理ツール pgAdmin 4CVE-2026-17351 である。NVD の主評価で CVSS 3.1 は 9.0 CriticalAV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H)。影響範囲は pgAdmin 4 の 9.13 以上 9.17 未満で、修正は 9.17 に入っている。

この記事では、NVD の記述(CVE-2026-17351)と pgAdmin の issue #10192 をもとに、何がずれていたのか、そして最初に提案された修正がなぜ効かなかったのかを整理する。後者のほうが、自分のコードに持ち帰れる話が多い。

掲載は 脆弱性対応ウォッチ にもある。

前段: CVE-2026-12045 が塞いだつもりだったもの

この CVE は単独の欠陥ではない。先行する CVE-2026-12045 の修正が、条件付きで迂回できたというものだ。順番に見る。

pgAdmin 4 の AI アシスタントには execute_sql_query というツールがある。LLM が SQL を組み立て、このツールへ渡すと実行される。当然ながら、LLM が書いた SQL をそのまま実行するのは危険だ。そこで 9.16 の修正はこう設計された。

  1. 渡された SQL を Python の sqlparse で構文解析する
  2. トランザクション制御を含まない単一文であることを確かめる
  3. 確かめてから BEGIN TRANSACTION READ ONLY で囲って実行する

読み取り専用トランザクションの中なら、たとえ何かおかしな SQL が来ても書き込みはできない。単一文だけを許すのだから、複数文を密輸することもできない。そういう設計である。

ずれていたのは「引用符の直前のバックスラッシュ」

破れた場所は、想像より地味なところだった。文字列リテラルの読み方である。

PostgreSQL は 9.1 以降、standard_conforming_strings を既定で on にしている。この設定が on のとき、引用符の直前のバックスラッシュはただの文字として扱われる。エスケープではない。

ところが sqlparse は、これを引用符のエスケープとして扱う

この一点だけで、次のようなペイロードが通る。NVD の記述が挙げている実例をそのまま引く。

1
SELECT '\';COMMIT;CREATE TABLE pwn(x int);SELECT 1 --'

2つの読み方を並べてみる。

sqlparse から見ると'\' の内側でバックスラッシュが次の ' をエスケープしているので、文字列リテラルは末尾の --'' まで続いている。つまり全体で 1つの SELECT 文である。検証は通る。

PostgreSQL から見るとstandard_conforming_strings = on なのでバックスラッシュはただの文字であり、文字列リテラルは '\' で閉じている。したがって残りは別々の文として読まれ、全体は 4つの文になる。

1
2
3
4
SELECT '\'
COMMIT
CREATE TABLE pwn(x int)
SELECT 1 --'

密輸された COMMIT が、囲っていた読み取り専用トランザクションをその場で終わらせる。以降の CREATE TABLE は読み取り専用の外で走る。そして囲いの末尾にあるはずの ROLLBACK は、閉じるべきトランザクションが既に無いので何もしない命令に変わる。

CVE-2026-12045 が塞いだはずの書き込み・RCE への道が、そのまま戻ってくる。

攻撃の運び方は「間接プロンプトインジェクション」

ここで注意したいのは、攻撃者が pgAdmin の画面に触る必要が無いことだ。

このペイロードを実行するのは LLM である。攻撃者がやることは、AI アシスタントが読みにいくかもしれない任意のオブジェクトに、この文字列を置いておくことだけだ。テーブルのコメント、カラム名、行の中身。どこでもよい。LLM がそれを読み、ツール呼び出しとして出力すれば成立する。

CVSS ベクタの PR:L(低い権限が必要)と UI:R(利用者の操作が必要)は、この運び方を反映している。攻撃者に要るのは、AI アシスタントが読む範囲に文字列を1つ置ける程度の権限であり、あとは正規の利用者が AI アシスタントを使うのを待てばよい。

効かなかった修正案 — psycopg3 の prepare_threshold

ここからが、この CVE のいちばん実務的な部分である。

最初に提案された修正案は、次のようなものだった。psycopg の execute(..., prepare=True) を使い、PostgreSQL 側の Parse ステップ(拡張問い合わせプロトコル)に複数文を拒否させる。 sqlparse がどう分類しようと、プロトコルの段階で複数文が通らなくなるなら、パーサのずれは無関係になる。筋は良い。

しかしこの修正案は、提出されたままでは何も塞がなかった。理由は psycopg3 の実装にある。

psycopg3’s PrepareManager silently ignores the prepare argument whenever the connection’s prepare_threshold is None

prepare_thresholdNone のとき、psycopg3 の PrepareManagerprepare 引数を黙って無視する。そして pgAdmin では、すべてのサーバ接続の既定値がまさに None である。サーバごとの「Prepare threshold」欄は、管理者が明示的に設定しない限り空欄のままだ。

結果として psycopg3 は単純問い合わせプロトコルへ落ちる。これは複数文を通せる、迂回がまさに使っていたのと同じ経路である。つまりこの修正案は、現実の既定構成では1件も塞いでいなかった

「黙って無視する」というのが厄介なところで、例外も警告も出ない。テストで書き込みが防がれたことを確認したつもりでも、prepare_threshold を明示していない環境では防がれていない。

採用された修正は、接続そのものの設定を変える

最終的に採られた修正は、AI アシスタントのツールが開く専用の使い捨て読み取り専用接続に対して、次を直接設定するものだった。

1
conn.prepare_threshold = 0

prepare 引数に頼るのをやめ、接続の属性を 0 にすることで、サーバ側の設定に関係なく拡張問い合わせプロトコルを強制する。構造的に単純問い合わせプロトコルへ落ちなくなる。

NVD の記述は、実機の PostgreSQL 18 で検証したと明記している。結果は次のとおりだ。

  • prepare_threshold = None(既定の挙動): 上のペイロードは実行に成功する
  • その接続に prepare_threshold = 0 を設定: cannot insert multiple commands into a prepared statement拒否される

エラーメッセージまで出ているので、自分の環境で確かめられる。手元の PostgreSQL で試すなら、次のような形になる。

1
2
3
4
5
6
7
8
9
10
11
12
13
import psycopg

PAYLOAD = "SELECT '\\';COMMIT;CREATE TABLE pwn(x int);SELECT 1 --'"

with psycopg.connect("postgresql://localhost/testdb") as conn:
# 既定では prepare_threshold は None。この状態では複数文が通る
print("prepare_threshold =", conn.prepare_threshold)
conn.prepare_threshold = 0 # 拡張問い合わせプロトコルを強制する
with conn.cursor() as cur:
try:
cur.execute(PAYLOAD)
except psycopg.errors.SyntaxError as e:
print("拒否された:", e)

prepare_threshold = 0 を設定した行をコメントアウトすると挙動が変わる。検証しているのはアプリのコードではなく、プロトコルの選択であるという点が確かめられる。

なお、これはあくまで挙動を確認するためのコードで、自分が管理する使い捨てのデータベースで実行すること。CREATE TABLE pwn が実際に走るので、本番や共有環境で試してはいけない。

対処

pgAdmin 4 を 9.17 以降へ更新する。9.13 より前を使っている場合はこの CVE の対象外だが、その場合は先行する CVE-2026-12045 の対象になりうるので、いずれにせよ上げるのが早い。

すぐに更新できない場合の最も確実な緩和は、AI アシスタント機能を無効にすることである。この欠陥は execute_sql_query の経路でしか成立しないので、機能を使っていない組織にとってはこれが実質的な回避策になる。

加えて、アプリ側の修正とは独立に効く多層防御がひとつある。AI アシスタントを使う接続には、本番の書き込み権限を持つロールを割り当てないことだ。専用の読み取り専用ロールを作り、そのロールで接続する。パーサのずれが再び見つかっても、そのロールにできること以上のことは起きない。

持ち帰れる話

この CVE から一般化できることが2つある。

ひとつめ。「事前検証してから実行系へ渡す」設計は、2つのパーサの解釈が完全に一致していないと成立しない。 sqlparse と PostgreSQL のように実装が別なら、食い違いは原理的に残り続ける。今回見つかったのは standard_conforming_strings に関する1件だが、これが最後の1件である保証はどこにもない。

安全側の設計は、検証を積み増すことではなく、実行系そのものに拒否させることである。ここで採られた prepare_threshold = 0 がまさにそれで、プロトコルの段階で複数文を構造的に不可能にしている。似た形の設計を自分のコードに持っているなら、「アプリで検証しているか」ではなく「実行系が拒否するか」で見直す価値がある。

ふたつめ。ライブラリが引数を黙って無視することがある。 prepare=True を渡したのに、接続の別の設定次第で無視される、という挙動は、ドキュメントを読んでいなければ気づけない。修正を入れたあと「効いていること」をどう確かめるかまで設計に含めないと、今回のように修正したつもりの状態ができあがる。

確かめ方は難しくない。防ぎたい入力を実際に流して、拒否されることをテストにすればよい。上のコードのように、エラーが出ることそのものをアサートする。「例外が出なかった」ではなく「期待した例外が出た」を検査対象にすると、黙って無視された場合にテストが落ちる。

参考

CVSS 3.1 は NVD の主評価・CNA いずれも 9.0 Critical で一致しており、CVSS 4.0 では CNA が 9.4 Critical を付けている。CWE は CWE-89(SQL インジェクション)と CWE-115(データの誤解釈)。CISA KEV への収載は、2026年8月26日時点のカタログ(2026.08.25 版)では確認できていない。


検証用パーサと実行系パーサがずれると、検証は素通りする — pgAdmin 4 の CVE-2026-17351
https://blog.hashito.biz/2026/08/26/pgadmin-cve-2026-17351-sqlparse-vs-postgresql-parser/
著者
hashito
作成日
2026年8月26日
著作権