Back to list
AI アージェントにデータベースアクセスを付与することは、思っているほど容易ではありません
Giving AI Agents Database Access Is Way Harder Than It Looks
Translated: 2026/4/25 0:01:49
Japanese Translation
12 ヶ月後の世界を想像してみてください。AI アージェントはありとあらゆる所にあり、本物に強力です。コードを書く、機能をリリースする、-production の問題をデバッグし、誰もがしたくない退屈な仕事の半分を静かにこなします。
しかし、問題はあります。アージェントの知能は、あなたに与えた世界の知能だけに依存します。見ることができないシステムについて論理的に推論することはできず、アクセスできるデータを持たないアクションを実行することもできません。
モデルは脳です。あなたが体と住む部屋を作る必要があります。
これは、今最も効果的なことは、最も賢いモデルを選び出すことではなく、アージェントが自由に動き回っても何も壊すことの無いサンドボックスを構築することであることを意味します。
そのサンドボックスの最も重要な一部は、データベースへのアクセスです。そして、それを安全に行うことは極めて難しいことを知りつつ、なぜそうなるのか、および私がそれを解決する方法を考えているのかについてこのポストで話します。
最初に AI アージェントを真正のデータベースに話させるために、私は read-only ユーザーを与え、終了を決めました。約 2 日間だけはそれが安全に感じましたが。
その後、実際に起こりうることをすべて書き出しました。リストは予想したの远比大でした。
モデルは確かに DELETE を書けるので、コメントトリックに包み、面倒な regex チェックをすり抜けてしまいます。「読まれない」状態がアプリコードで強制されている場合でも、単一の文字一致でも read-only である限りではありません。
モデルの出力に信頼するようになると、その隙間は目立たなくなります。
「read-only」な Postgres ユーザーは、SELECT pg_sleep(3600) を実行して静かに接続を開き続けることができます。これを数回行えば、プールが壊れ、実際のアプリが全員に 500 エラーを示し始めます。
あるいは、アージェントが 12 テーブルのカルティアン結合を記述し、5 億 1 千万行のデータセットを返します。それは技術的に違法ではありませんが、メモリ不足(OOM)を招き、シェアリングしているデータベース全体をダウンさせます。
ユーザーと oauth_tokens をまたぐ完全に無害に見える結合操作は、アプリが決して公開しようとはしなかった認証情報を引きずり出します。クエリは有効な SQL です。ユーザーには権限があります。しかし、結果はセキュリティインシデントです。
このものは私が夜中まで目覚ましくさせるものです。なぜなら、それが捉えるような構文チェックが存在しないからです。
私の個人的なお気に入りです。あなたの support_messages テーブルの 1 行にあり、それがアージェントを別の接続に完全にクエリすることを説く文字列が含まれています。
攻撃者はあなたのユーザーではありません。攻撃者は、3 ヶ月前に誰かが書いたテキストであり、それが今データベースに存在しているのです。
そして、これらは私が思い付いた失敗だけではありません。恐ろしいものは、私が思い付かなかったものです。
より深い問題は、単一の欠落したチェックにあるのではなく、あなたが頼りとする各レイヤーが独自に打ち砕かれる可能性があるという点にあります。
静的な SQL パースは、知らなかった動的なパターンを捉えられません。「read-only」Postgres ロールでも、あなたの CPU を溶かすクエリを止めることはできません。
アプリレベルのタイムアウトは、ネットワークコールがフリーズする場合は発火しません。レートリミットは、実際に通り抜ける 1 つのクエリがすべてのデータを読む場合は助かりません。
どれか 1 つだけでもそれは紙の壁です。それを厚くするだけでそれを修正することはできません。あなたはより多くの壁を追加することで修正します。
私にとって最終的に理解できたメンタルモデルは、玉ねぎです。
あなたが可能だとしても最も制限された状態から始めます。デフォルトはテーブルなし、カラムなし、書き込みなし、何もありません。
そして、アージェントが実際に必要とする能力だけを、それ以外のものは除外して剥き出します。デフォルト拒否は正直に 90% の作業を行っています。
パーサは 1 つのレイヤーです。データベースレベルの read-only トランザクションは別のレイヤーです。ステートメントタイムアウト、行数制限、カラム許可リスト、事前実行コスト推定値、監査ログです。
それぞれが存在するのは、それ以前のレイヤーが失敗する可能性があるからです。それがポイントです。あなたは 1 つの完璧なチェックを構築していません。故意に不完美なものを重ねています。
「ハッピーパスが通り抜ける」わけではありません。敵対テストケース、実世界のリストから抽出されたプロンプトインジェクションペイロード、マルチステートメント攻撃、外見は良いが実はダメなクエリ、外見はダメだが実は良いクエリです。
玉ねぎの全体的なポイントは、各レイヤーが最終的に打ち砕かれると仮定し、それよりも下のレイヤーがその落下を捉えることを保証することです。
あなたが最初のレイヤーだけで構築した場合、あなたは薄っぺらいものを作っ
Original Content
Imagine the world 12 months from now.
AI agents are everywhere and they are genuinely powerful. They write code, ship features, debug production issues, and quietly do half of the boring work nobody wanted to do anyway.
But there's a catch. Agents are only as smart as the world you give them. They can't reason about a system they can't see, and they can't act on data they don't have access to.
The model is the brain. You're the one who has to build the body and the room it lives in.
Which means the highest-leverage thing you can do right now is not picking the smartest model. It's building a sandbox where your agents can roam free and never break anything.
One of the most important parts of that sandbox is access to your databases. And it turns out doing that safely is extremely hard. This post is about why, and how I think about solving it.
The first time I let an AI agent talk to a real database, I gave it a read-only user and called it a day. That felt safe for about two days.
Then I sat down and actually wrote out everything that could go wrong. The list was a lot longer than I expected.
A model can absolutely write a DELETE, wrap it in a comment trick, and slip it past a sloppy regex check. "Read-only" enforced in app code is one bad string match away from being not-read-only.
And once you start trusting the model's output, that gap is hard to see.
A "read-only" Postgres user can still fire off SELECT pg_sleep(3600) and quietly hold a connection open. Do that a few times and your pool dies, and your real app starts 500'ing for everyone.
Or the agent writes a 12-table cartesian join that returns half a billion rows. Nothing about that is technically illegal, but it'll OOM the box and take the database down for everyone sharing it.
A perfectly innocent-looking join across users and oauth_tokens can yank credentials your app never intended to expose. The query is valid SQL. The user has permission. The result is a security incident.
This one keeps me up at night because there's no syntax check that catches it.
My personal favorite. A single row in your support_messages table contains a string that convinces the agent to query a different connection entirely.
The attacker isn't your user. The attacker is text someone wrote three months ago that's now sitting in your database.
And those are just the failures I thought of. The scary ones are always the ones I didn't.
The deeper problem isn't any single missing check. It's that every layer you'd lean on can be defeated on its own.
Static SQL parsing can't catch dynamic patterns it doesn't know about. A read-only Postgres role won't stop a query from melting your CPU.
App-level timeouts don't fire if the network call hangs. Rate limits don't help if the one query that does make it through reads everything.
Any one of those alone is a paper wall. And you can't fix that by making one wall slightly thicker. You fix it by adding more walls.
The mental model that finally clicked for me was an onion.
You start as restrictive as you possibly can. The default is no tables, no columns, no writes, no nothing.
Then you peel back exactly the capabilities the agent actually needs, and nothing else. Default-deny is honestly doing 90% of the work.
The parser is a layer. The database-level read-only transaction is a layer. The statement timeout, the row limit, the column allowlist, the pre-execution cost estimator, the audit log.
Each one exists specifically because the layer in front of it can fail. That's the point. You're not building one perfect check. You're stacking imperfect ones on purpose.
Not "happy path passes." Adversarial test cases. Prompt injection payloads pulled from real-world lists. Multi-statement attacks. Queries that look fine and aren't. Queries that look bad but are actually fine.
The whole point of an onion is to assume every layer will eventually be defeated, and to make sure the layer underneath it catches the fall.
If you only build the first layer, you've built a wall. If you build all of them, you've built a building.
QueryBear's stack is built around exactly this idea. I won't go into every gory detail, but the layers we run today include:
A SQL parser that strictly only allows what we expect.
An allowlist of tables and columns the agent is actually permitted to see.
AST-level rewriting so row limits and timeouts can't be bypassed by the agent.
A pre-execution cost check that rejects queries that would scan too much.
Database-level read-only transactions and statement timeouts as the hard backstop.
A full audit log so we (and you) can replay everything later.
There are a few pieces I'm not putting in a blog post. Some of the secret sauce is in how the layers interact, and in how we test against attacks that haven't been invented yet.
This is the part of the product I'm most paranoid about, and honestly the part I'm most proud of.
Don't trust a single guardrail. Stack them. Default-deny everything, and test it like someone is actively trying to break it.
Because eventually, someone will be.