AIエージェントはどこを攻撃されるか?
脅威モデルは、攻撃者が何に届くかと、その方法を書き出したものです。エージェントは、チャットボットより大きな被害を出しえます。メールを読み、ツールを呼び、外へ何かを送るからです。
「LLMアプリケーションの安全対策」で、インジェクションと主なリスクを学びました。このレッスンでは、それを1つのエージェントの地図にします。コードを書く前に描きます。
このレッスンのゴール
メモ
エージェントが攻撃される場所を地図にできる。
キーワード
| ことば | いみ | れい |
|---|---|---|
| 守る対象 | 顧客リストや支払いツールなど、守る価値のあるもの | |
| 信頼境界 | 自分で管理できない所から、文字や呼び出しが入る線 | |
| 信頼できない内容 | 見知らぬ人が書いたかもしれない、エージェントが読む文字 | Webページ |
| 出口 | エージェントが持ち主の外へデータを送れる経路 | |
| 脚 | 危険な組み合わせを作る3つのうちの1つ |
学生が入力するセルは、このプレビューでは空白です。
危険な3つの組み合わせ
3つがそろうと危険です。エージェントが非公開データを持つこと。信頼できない内容を読むこと。データを送れる出口があることです。
3つのうち2つなら、データを持ち出す攻撃は止まります。文字でエージェントを動かせても、何も出ていかないか、盗む物がありません。3つそろうと、隠された1行でデータを持ち出せます。
例: 3つのエージェント
| エージェント | 非公開データ | 信頼できない内容 | 出口 |
|---|---|---|---|
| メール要約 | 受信箱 | 知らない人のメール | なし。要約を見せるだけ |
| Web担当 | なし | どのWebページも | チャットに投稿できる |
| 受信箱ヘルパー | 受信箱 | 知らない人のメール | 返信を送れる |
学生が入力するセルは、このプレビューでは空白です。
Runは脚を並べるだけです
最初のスクリプトは、架空のエージェント4つを、ただのデータとして持ちます。それぞれの脚を表示し、3本そろったものに印を付けます。
モデルは呼びません。攻撃もしません。脅威モデルは「何が起こりうるか」を考えるものです。練習にはスクリプトの中の一覧で十分です。
脚のスクリプトを書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' '// 架空のエージェントが4つ。各フラグは、そのエージェントにできることを表します。' 'const agents = [' ' { name: \"mail-summary\", data: true, untrusted: true, out: false },' ' { name: \"web-clerk\", data: false, untrusted: true, out: true },' ' { name: \"report-bot\", data: true, untrusted: false, out: true },' ' { name: \"inbox-helper\", data: true, untrusted: true, out: true },' '];' > legs.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
脚のスクリプトを書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function legs(a) {' ' const found = [];' ' if (a.data) found.push(\"非公開データ\");' ' if (a.untrusted) found.push(\"信頼できない内容\");' ' if (a.out) found.push(\"外への出口\");' ' return found;' '}' 'for (const a of agents) {' ' const all = legs(a).length === 3;' ' console.log(a.name + \": \" + legs(a).join(\"、\") + (all ? \" <- 3つとも\" : \"\"));' '}' >> legs.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
脚のスクリプトを実行
docker exec -w /home/node/project agent node legs.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
スクリプトの出力
| 行 | 意味 |
|---|---|
| mail-summary: 非公開データ、信頼できない内容 | 脚は2本。出口はなし |
| web-clerk: 信頼できない内容、外への出口 | 脚は2本。非公開データはなし |
| report-bot: 非公開データ、外への出口 | 脚は2本。入力は信頼できるものだけ |
| inbox-helper: 3本とも、そして <- 3つとも | 最初に直すのはこれだけ |
学生が入力するセルは、このプレビューでは空白です。
脚を外す。お願いはしない
直し方は、脚を1本外すことです。出口をなくすか、信頼できる情報源だけを読むか、非公開データを届かない所に置きます。いちばん安いものを選びます。
「絶対に漏らさないで」というプロンプトは対策ではありません。1回の実行で確率を下げるだけで、次の実行は保証しません。
よくある間違い
注意
入力はユーザーが打った文字だけだと考える人が多いです。ツールの結果、ファイル、ページ、前のメッセージも入力です。どれも、見知らぬ人が書いた可能性があります。
モデル用スクリプトを書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '' 'const text = \"あるアシスタントは、顧客のメールを読み、要約し、公開チャットルームにリンクを投稿できます。\";' 'const messages = [' ' { role: \"system\", content: \"ちょうど3行で答えてください。1行目は data:、2行目は untrusted:、3行目は out: で始めます。コロンの後に はい か いいえ を書いてください。\" },' ' { role: \"user\", content: \"このエージェントは、非公開データを持ち、信頼できない内容を読み、外への出口を持っていますか。\" + text },' '];' '// 記録済みサンプル。モデルが答えないときだけ使います。' 'const sample = \"data: はい\nuntrusted: はい\nout: はい\";' > legs2.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
モデル用スクリプトを書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function parse(reply) {' ' const found = {};' ' for (const line of reply.split(\"\n\")) {' ' const bits = line.split(\":\");' ' if (bits.length === 2) found[bits[0].trim().toLowerCase()] = bits[1].trim().startsWith(\"はい\");' ' }' ' return found;' '}' '' 'async function main() {' ' const reply = await ask(messages, sample, 60);' ' console.log(\"返答:\n\" + reply);' ' const f = parse(reply);' ' const keys = [\"data\", \"untrusted\", \"out\"];' ' const shape = keys.every(function (k) { return k in f; });' ' console.log(\"形は正しいか: \" + shape);' ' if (shape) console.log(\"モデルによる3つとも: \" + (f.data && f.untrusted && f.out));' ' console.log(\"プログラムは、この返答ではなく自分のツール一覧を信じます。\");' '}' 'main();' >> legs2.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
モデル用ヘルパーを書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' 'async function ask(messages, sample, max) {' ' try {' ' const res = await fetch(base + \"/v1/chat/completions\", {' ' method: \"POST\",' ' headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' ' body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 200 }),' ' signal: AbortSignal.timeout(30000),' ' });' ' if (res.ok === false) throw new Error(\"status \" + res.status);' ' return (await res.json()).choices[0].message.content;' > model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
モデル用ヘルパーを書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' ' } catch (error) {' ' console.log(\"記録済みサンプル: モデルが答えませんでした (\" + error.message + \")。\");' ' const list = [].concat(sample);' ' const turn = messages.filter(function (m) { return m.role === \"assistant\"; }).length;' ' return list[Math.min(turn, list.length - 1)];' ' }' '}' 'module.exports = { ask };' >> model.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
モデルで実行
docker exec -w /home/node/project agent node legs2.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
出力の一例
| 行 | 表示されうる内容 |
|---|---|
| 返答 | data、untrusted、out で始まる3行。コロンの後は毎回変わります |
| 形は正しいか | 3行そろえば true。false なら、聞き直すか断ります |
| 記録済みサンプル | モデルが答えないとき、最初に出ます。サンプルで続きます |
学生が入力するセルは、このプレビューでは空白です。