pg-readonly-mcp
pg-readonly-mcp
SQLをパターンマッチングではなく解析する、読み取り専用のPostgres MCPサーバー。なぜなら、代替手法は過去に実際に公の場で失敗しており、その具体的な内容を明確にしておく価値があるからです。
このプロジェクトが塞ぐバイパス
AnthropicのリファレンスPostgres MCPサーバーは、各クエリを読み取り専用のトランザクションでラップすることで「読み取り専用」を強制していました。また、セミコロン区切りの複数ステートメント入力も受け付けていました。この組み合わせは悪用可能です。
SELECT 1; COMMIT; DROP SCHEMA public CASCADE;COMMITは読み取り専用トランザクションを早期に終了させます。それ以降のすべては完全なセッション権限で実行されます。Datadog Security Labsは2026年にこれを公開しました。このサーバーは非推奨となりアーカイブされましたが、脆弱性のあるパッケージはその後も毎週21,000ダウンロードされ続けていました。
読み取り専用トランザクションはクエリの実行方法の特性です。クエリの内容については何も保証しません。だからこそ、そこから抜け出すことが可能だったのです。このサーバーは代わりに、2つ目の要素(クエリの内容)をチェックします。
Related MCP server: postgres-mcp-readonly
独立した2つの層
どちらか一方だけでも、公開されたバイパスを防ぐことができました。両方存在するのは、片方の欠陥だけがエージェントと書き込み操作の間の唯一の障壁であってはならないからです。
1. SQLはスキャンではなく解析される。 guard.pyはsqlglotを使用して実際の構文木を構築し、3つのチェックを適用します。
正確に1つのステートメント。
sqlglot.parseは、ドライバと同じようにステートメント終端のセミコロンで分割するため、上記のペイロードは3つのステートメントになり、それらが接続に到達する前に拒否されます。外側のステートメントは読み取り形状 —
SELECT、UNION、INTERSECT、EXCEPT、またはこれらから構築されたCTE。DROP TABLE usersはここで拒否されます。ツリー内のどこにも書き込みがないことを、完全に走査して確認。これが他の2つのチェックでは代替できない部分です。PostgresではCTEがデータ変更ステートメントを含むことができるため、
WITH x AS (DELETE FROM t RETURNING *) SELECT * FROM xは、外側のレベルではSELECTです。外側の形状だけを見るチェックはこれを見逃します。すべてのノードを走査することで、DELETEが何階層下にあっても見つけ出します。
認識できないステートメント形状 — sqlglotに固有のルールがないもの — は、他のすべてと同様のルールで拒否されます。馴染みがないことは安全と同じではありません。
2. 接続自体が書き込みできない。実行内容とは独立しています。check_connection_is_readonlyは、接続されたロールがスーパーユーザーである場合、データベースやロールを作成できる場合、行レベルセキュリティをバイパスできる場合、または表示可能なテーブルに対してSELECTを超える権限を保持している場合に、起動を拒否します。網羅的ではありません(Postgresの権限は所有権、PUBLIC権限、またはこのチェックで列挙されていないRLSポリシーを通じて付与されることもあります)が、最も一般的な誤設定の2つの形態を捉えます。これらは暗黙的に示唆するのではなく明示されています。
保護しないもの
特権的な接続文字列が渡された場合。 起動チェックは過剰権限の一般的な形状を捉えますが、網羅的な権限監査ではなく、その旨が上記で明確にされています。
制限内でのリソース枯渇。 合法的に1,000行の非常に幅広いデータを返すクエリや、計画的に正当に高コストなクエリは、そのコストがかかります。行数上限とステートメントタイムアウトは被害を制限しますが、高コストな読み取りを無料にはしません。
エージェントがデータを取得した後にどうするか。 これはクエリゲートであり、データ損失防止ツールではありません。テーブルへの読み取りアクセスは、そのテーブル内のすべてへの読み取りアクセスです。
パーサーの食い違い。
sqlglotとPostgres自身のパーサーは、同じ文法の独立した2つの実装です。Postgresが受け入れるすべてのエッジケースで一致することが証明されているわけではありません — 単一層を信頼しないというプロジェクト全体の前提における、実際の(ただし狭い)ギャップです。層2は部分的にこのために存在します。パーサーの差異によって意図しないものがガードを通り抜けたとしても、その下の接続は依然として書き込みできないからです。
スキャナー結果
2026年8月18日にagent-audit 0.19.2で実行:15件の検出、11件は自動抑制、4件は対処が必要 — 1件のBLOCK、3件のWARN。
BLOCKの検出が最も興味深く、全文を読む価値があります。 server.py:156、信頼度1.0:cur.execute(sql) — パラメーター化されていない実行によるSQLインジェクションとしてフラグ付けされています。その行は実際に存在します。しかし、このコードベースの中で最も防御された行でもあります:sqlがそこに到達するまでに、validate_readonly()はすでにそれを解析し、正確に1つのステートメントであることを確認し、そのステートメントが読み取り形状であることを確認し、書き込みがないかすべてのノードを走査しています。スキャナーはそのいずれも認識できません — 単一ファイルのパターンマッチであり、検証は別の関数、別のモジュールで、数行前に行われています。スキャナーは、一般的に危険な形状を正しく識別しますが、その形状がすでにチェック済みであることは認識できません。
また、スキャナーが提案する方法を採用しても問題は解決できませんでした。 パラメーター化クエリは、固定されたクエリ形状に代入される値を保護します — WHERE id = %s。ここでは適用できません。なぜなら、このツールが受け入れる入力はクエリの構造だからです。「呼び出し元が要求した任意の読み取り専用SQLを実行する」ための値のみのパラメーター化スキームは存在しません。この種のツールの修正方法は構造を検証することであり、それがこのファイルの残りの部分の目的です。
query()の定義に対するWARN(AGENT-034、「関数本体に入力検証がない」)は、別の角度からの同じ盲点です。関数の最初の行はtry/except内でvalidate_readonly(sql)を呼び出しています。インポートされた関数の呼び出しは、スキャナーが検証として認めるパターンではありません。
ハードコードされた資格情報に関する2つのWARNはtests/conftest.pyにあります。デフォルトの管理者DSN(postgres:postgres@localhost:5432/postgres、標準のローカル/CI Postgresデフォルト)と、テストスイートが同じテスト内で作成および削除するロールに使用されるリテラルパスワードです。どちらも資格情報のような文字列として正しく識別されていますが、どちらも何かを保護する資格情報ではありません — 1つは使い捨てのローカルデータベースを指し、もう1つは単一テストの存続期間のみ存在します。
残りの11件の検出 — すべてAGENT-041、すべてuuid.uuid4()由来の名前からCREATE SCHEMA / GRANT / DROP ROLEステートメントを構築するテストフィクスチャ内 — は、スキャナー自体によってすでに自動抑制されていました。
パターンスキャナーは煙探知機であり、裁判官ではありません。検出結果とその理由を公開することは、単に綺麗な数字を出すことよりも価値があります。
インストール
pip install -e .設定
接続文字列が必要です。--dsnで指定するか、PG_READONLY_MCP_DSN環境変数で指定します。環境変数は、パスワードがコマンドラインや誤ってコミットされる可能性のあるクライアント設定ファイルに現れないようにするために存在します。
{
"mcpServers": {
"pg-readonly-mcp": {
"command": "pg-readonly-mcp",
"env": { "PG_READONLY_MCP_DSN": "postgresql://readonly_role:...@host:5432/db" }
}
}
}その接続文字列内のロールはSELECT以外の権限を持っていてはいけません。 サーバー自身がこれをチェックし、満たされない場合は起動を拒否します — 上記のcheck_connection_is_readonlyを参照してください。
ツール
ツールは意図的に1つだけです。全価値提案が「読み取り以外はすべて拒否する」というサーバーに、それを正しく実現するために2つ目の表面領域は必要ありません。
ツール | 機能 |
| 1つのSELECT形状のステートメントを実行します。接続に到達する前に解析および走査されます。 |
テスト
37のテスト。25はデータベース不要でどこでも実行可能 — これらはguard.pyの全テストスイートであり、純粋な文字列入力、判断出力です。 残りの12は実際のPostgresが必要であり、設計上統合テストです。check_connection_is_readonlyのポイントは、実際のロール属性と実際の権限に対して何を行うかであり、モックされた接続ではサーバーが実際の接続に対して行う動作に関係なくパスしてしまいます。
pytest -q --cov=pg_readonly_mcpCIは実際のpostgres:16サービスコンテナに対して実行され、データベースバックエンドのテストがそこでスキップとして報告された場合、ビルドは失敗します — これはsweep-mcpがそのシンボリックリンクテストに適用するのと同じルールであり、同じ理由です。何もしないテストは、テストがないことよりも悪いのです。
12のテストの中には、Datadogのペイロードそのものを、guard.py経由ではなく実際のMCPツール呼び出しを通じて再現するライブ再現テストがあります。そして、ターゲットスキーマがその後も存在することをアサートします(例外が発生したことだけでなく)。また、同じ呼び出しパスを通じてCTE内に隠された書き込み、行数上限、ステートメントタイムアウト、キャンセルされたクエリの後も接続が次のクエリに使用可能であること、CREATEDBを持ちテーブル権限がゼロのロールがロール属性のみで拒否されることもカバーされています。
実際のPostgresに対するCIでのカバレッジ:78%、guard.pyは100%。 server.pyのギャップは、汎用的なpsycopg.Errorキャッチオール — スイート内でキャンセル以外のデータベースエラーを意図的に引き起こすものはありません — とmain()のargparseおよびトランスポート配線であり、スイートは代わりにbuild_serverを直接使用してこれらを実行します。トランスポートはモックする価値が最も低く、実際のミスが隠れる可能性が最も低い部分です。
この最初のプッシュバージョンはパスしませんでした。 実際のPostgresサービスコンテナが初めてスイートを実行したときにのみ2つのバグが表面化し、どちらもコードだけからは見えませんでした。SET statement_timeout = %sはPostgresにSET statement_timeout = $1として到達し、解析に失敗しました。なぜならSETはユーティリティステートメントであり、SELECTのようにバインドパラメータを受け付けないからです。ツールへのすべての実際の呼び出しが同様に失敗していたでしょう。別途、テストフィクスチャがまだ有効な権限を持つロールに対してDROP ROLEを試み、Postgresが拒否しました。先にDROP OWNED BYを実行する必要があります。両方とも修正され、それ以降の実行がこれらの数値の元となっています。これは、成功だけを報告するスイートは誰も失敗を見たことがないスイートであるため、残しています。
レイアウト
src/pg_readonly_mcp/
guard.py parses and walks the tree. Opens no connection. 135 lines.
server.py the MCP tool, the connection check, the timeout and row cap.
tests/
test_guard.py 25 tests - no database, run anywhere
test_server.py 12 tests - live Postgres required, CI-enforcedguard.pyはMCPやpsycopgについて何も知りません。クエリがserver.py内の理由で拒否された場合、それはバグです。判断は1層下に属し、文字列だけでテスト可能であるべきです。
ライセンス
MIT。
This server cannot be deployed
Maintenance
Related MCP Connectors
- WoWSQLOAuthcom.wowsql
Managed Postgres MCP: projects, SQL, docs search, and storage buckets via OAuth.
Safe, read-only Postgres and MySQL access for AI agents. Audit log + column-level controls.
Guard AI agents' PostgreSQL/MySQL access via MCP: SQL audit, auth, masking, write approval
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceRead-only PostgreSQL MCP server that enables running SELECT queries, listing tables and schemas, and describing columns, with built-in protection against writes and malicious SQL attacks.590 npmMIT
- AlicenseAqualityDmaintenanceA secure, read-only PostgreSQL MCP server that provides safe database introspection and querying capabilities.1419 npmMIT
- FlicenseNot gradedqualityBmaintenanceRead-only MCP server for PostgreSQL enabling schema introspection and SELECT queries via MCP clients like Claude, with multi-layered write protection.-
- AlicenseNot gradedqualityCmaintenanceProvides a read-only PostgreSQL MCP server with schema introspection. Enforces least-privilege database roles to prevent any writes, even from malicious SQL.MIT