コーディングエージェントの権限制御の仕組みを再発明したら、ただのイタチごっこだった
Claude Codeの自動承認機能に不安があり、同じような仕組みを自分でも作ってみました。しかし、実際に使ってみると、権限制御というのは思っていたより単純な問題ではなく、新しい穴を見つけては防ぐの繰り返しでした。この取り組みの中で考えたことを書き残したいと思います。
🎧 この記事の内容について、AIがポッドキャスト風に解説した音声です。
Table of Contents
Claude Codeの権限設定に対する不安
Claude Codeはツール呼び出しの自動承認設定をサポートしており、例えば Bash(find *) のように許可リストに記載することで、ユーザーへの確認をスキップしてfindコマンドを実行できるようになります。
ここで問題になるのが、findコマンドには -exec というオプションがあり、見つかったファイルに対して任意のコマンドを実行する機能があるということです。つまり、findを許可するということは全てのコマンド実行を許可しているのと同じなのです。
また、.gitignoreに記載されたバージョン管理されないファイルの扱いも気になるポイントでした。ファイルの読み書きを許可したら、特に特別扱いはされず承認はスキップされます。(※ 現在は何かしら対策が入っているかもしれません)何が困るかというと、.envのようなGit管理外の開発用のシークレットを勝手に読んでしまうのです。
これらを踏まえて、以下のような制御が可能なエージェントを作ることにしました。
- コマンドの実行を条件付きで許可(
findは許可するが-execオプションは許可しない) - Git管理外のファイルアクセスにはユーザーの承認を必須化
試作した権限制御の仕組み
1. 入力をチェックしやすいコマンド呼び出しのInterface設計
ソフトウェアには不具合がつきもので、入力チェックの容易性が不具合のない安全な権限制御の実装につながると考え、まずはInterfaceを考えました。
工夫したのは、コマンドに対する引数を文字列の配列として受け取ること、また、ワイルドカード展開などのシェルの機能はサポートしない、というところです。これによって例えば cat .*のような呼び出しはできず、 cat .env のような明示的なファイル指定を必須化します。また、 echo foo; rm -rf / のようなシェルによる複数コマンドの実行防ぐことができます。
{
name: "exec_command",
description: "Run a command without shell interpretation.",
inputSchema: {
type: "object",
properties: {
command: {
description: "The executable name or path. e.g., rg",
type: "string",
},
args: {
description: "Array of arguments to pass to the command.",
type: "array",
items: {
type: "string",
},
},
},
required: ["command"],
}2. 入力項目がワーキングディレクトリ内のGit管理対象ファイルであることの確認
明示的なファイルパスの指定がされているという前提で、全てのコマンド引数をファイルパスとして扱い、それがワーキングディレクトリ内であること、また、Git管理されていることを確認する仕組みを作りました。
※ 例えば、 ls -l のオプション -l もワーキングディレクトリからの相対パスとして扱われますが、 .gitignore に記載されないので、Git管理対象と判定されます。
3. 正規表現ベースの自動承認設定
そして、条件付きでコマンドを許可できるように、以下のような仕組みを作りました。
- ツール名
toolNameと入力inputのパターンとマッチした際のaction(承認、拒否、ユーザーに確認)を設定 - 正規表現
$regexと配列要素の有無確認$hasのサポート
この仕組みにより、 fd は許可するが、Git管理外のファイルやコマンド実行のオプションはユーザーに確認を求める、という設定が可能になります。※ find を例に話してましたが、デフォルトでGit管理外のファイルを無視する fd や rg を使うことで、 grep -r KEY ./ のように明示的なパス指定なしで .env の中身を読むような操作も防ぐことができます。
{
"autoApproval": {
"defaultAction": "ask",
"maxApprovals": 50,
"patterns": [
{
"toolName": "exec_command",
"input": { "command": { "$regex": "^(find|grep)$" } },
"action": "deny",
"reason": "Use rg or fd instead"
},
{
"toolName": "exec_command",
"input": {
"command": "fd",
"args": {
"$has": {
"$regex": "^(--unrestricted|--no-ignore|--exec|--exec-batch|--follow|-[^-]*[uIxXL])"
}
}
},
"action": "ask"
},
{
"toolName": "exec_command",
"input": {
"command": "rg",
"args": {
"$has": {
"$regex": "^(--unrestricted|--no-ignore|--follow|-[^-]*[uL])"
}
}
},
"action": "ask"
},
{
"toolName": "exec_command",
"input": {
"command": {
"$regex": "^(echo|ls|cat|head|tail|wc|date|pwd|uname|fd|rg|jq)$"
}
},
"action": "allow"
}
]
}
}権限制御の落とし穴
実際にエージェントを使う中で、さまざまな問題が見つかりました。
落とし穴 #1 拒否すべきパターンの考慮もれ
これは恥ずかしい話ですが、当初書いたルールでは rg -HI (—-hidden と -—no-ignore の組み合わせ) のような、オプションの省略表記の組み合わせや、 —-follow (シンボリックリンクをたどって、ワーキングディレクトリ外にアクセスする可能性がある)を拒否できてませんでした。
汎用性が高く、多様なオプションのあるコマンドは拒否すべきパターンを漏れなく考慮することは難しいと認知し、別のアプローチで解決すべきでした。 rg の例では、多くのコーディングエージェントがそうしているように、機能制限版のGrep toolを実装するアプローチにより、こういったすり抜けを軽減することができると考えています。
落とし穴 #2 エージェントが生成したコードによる権限設定のバイパス
こちらは最初から気がついていたので許可はしてませんでしたが、 package.json の scripts や Makefile に定義したタスクの実行を許可すると、ワーキングディレクトリ内であることやGit管理対象であることのチェックは無意味になります。
コーディングエージェントであれば、ファイルの読み書きを自動許可するのは普通のことかと思います。ファイルを変更できるということは、 package.json を書き換えることで npm run lint などの内容を変更できてしまいます。では、 package.json を書き換えられないように deny の設定をしたらどうでしょう?
{
"toolName": { "$regex": "write_file|patch_file" },
"input": { "filePath": { "$regex": "package.json" } },
"action": "deny"
}許可されているのがlinterの実行くらいであれば問題ないかもしれません。しかし、 npm run test のようなエージェントが生成したコードの実行を許可するような設定があるとすべて破綻します。 package.json を書き換えることもできますし、コードを通してワーキングディレクトリ外やGit管理外のファイルの読み書きや、任意のコマンドを実行することも可能です。
Sandbox環境
次に用意したのが、エージェントが暴走した際に被害を最小化するためのSandbox環境です。デフォルトではワーキングディレクトリでのReadのみを許可、書き込みやネットワークアクセスは明示的な許可が必要、という仕様です。
agent-sandbox --dockerfile Dockerfile \
--allow-write \
--allow-net registry.npmjs.org \
npm installこれを使うことで、ワーキングディレクトリの外は機密情報がなく、仮に .env 内の機密情報を読んでも外部には送信できない、という環境を作ることができます。エージェントの設定ではコマンド単位でネットワークアクセスなどの権限を指定可能です。
{
"sandbox": {
"command": "agent-sandbox",
"args": ["--dockerfile", ".agent/sandbox/Dockerfile", "--allow-write"],
"separator": "--",
"rules": [
// ghコマンドはサンドボックス外で動かす ※ ユーザ承認必須の前提
{
"pattern": {
"command": "gh",
},
"mode": "unsandboxed"
},
{
"pattern": {
"command": "npm",
"args": ["ci"]
},
"mode": "sandbox",
"extraArgs": ["--allow-net", "registry.npmjs.org"]
}
// マッチしないものはすべてサンドボックス環境で実行
]
}
}エージェントそのものをサンドボックスで動かすか、コマンドをサンドボックスで動かすか?
私のアプローチではコマンド単位でサンドボックス上で動かす方式にしましたが、dev containerなどのコンテナ環境内でエージェントを動かすというアプローチも広く採用されている方式かと思います。ユースケースによる部分もありますが、コンテナ環境でエージェントを動かすアプローチには懸念があります。
前提として、守るべきポイントは2つあると考えてます
- ファイルシステムへのアクセス:機密情報を読ませない
- ネットワークアクセス:
- 万が一機密情報を読んでしまっても、ネットワークを通して外に送らせない
- プロンプトインジェクションの危険性がある信頼できないドメインにアクセスさせない
実際の同僚たちのユースケースをみると、GitHub CLIを操作させてGitHubのissueをコンテキストとして与えたり、Pull Requestをエージェントに作らせる、ということをやっています。これをコンテナ環境でやろうとすると、GitHubの認証情報をコンテナ内に置いて、github.comやapi.github.comへのアクセスを許可することになります。これまでに考えてきたように、エージェントが生成したコードの実行権限を与えた時点で、認証情報を読ませないというのはほぼ不可能ではないでしょうか?そして、github.comへの通信を許可するということは、万が一トークンの権限設定に不備がある場合、例えば攻撃者が用意したリポジトリのissueへのコメントやgistなどを通して、ソースコードやGitHubの認証情報を持ち出し可能になるということです。繰り返しになりますが、GitHub CLIの実行を自動承認してなくても、エージェントが生成したコードから気が付かずに実行される可能性は残ります。
おわりに
いろいろ考えたが、実際のところ情報漏洩は起きるのか?
使い方にもよりますが、実際のところ確率は極めて低いと考えています。(= 考えなくて良い、対策しなくて良いというわけではない)直近1年、GPT、Claude、Geminiなどの主要なモデルやKimi、GLMなどのオープンモデルなども実務で試してきましたが、指示に従わずに情報を外に持ち出ししようとしたことは一度もありません。もし起こる可能性があるとしたら、プロンプトインジェクションでしょうか。偶然、攻撃の指示が書かれたコンテンツを読み込んでしまって、というケースです。
Webコンテンツからのプロンプトインジェクションに関しては各社対策をしていまして、例えば、Claude CodeではWebページを読み込む際、メインのコンテキストとは分けて必要な情報のみをメインのコンテキストに渡すという工夫をしているそうです。私のエージェントでも ask_google というツールを用意して、エージェントがGeminiを通してGoogle検索の結果を要約したものだけを受け取れるように工夫しています。
結局、自動承認機能なんてないほうが良いのではないか?
様々な工夫を重ねる中で、設定ミスのリスクがあり、これで防御できているという偽の安心感を与えてしまうような機能はむしろ害なのではないかとも考えました。エージェントの行動はコントロール不能であるという前提で、エージェントが動くサンドボックス環境のほうで防御するということです。
これに関してはまだ答えは出せてません。今のところは、GitHubのissueやコメントを直接読ませるなど、利便性を求めるとコマンド単位での承認・確認の設定は有用であると考えています。