このアシスタントはどこで失敗するか
大規模言語モデル(LLM)を使うアプリの主なリスクは、インジェクション、安全でない出力、過剰な権限、データ漏えい、過剰利用の五つです。どれも設計の特定の場所に潜んでいるので、その場所で探せます。
あるチームがサポートボットを公開し、うまくいく場合だけを試しました。レビューは別の問いを立てます。図の箱ごとに、何が入ってきて、その箱に何ができるのか、です。
このレッスンのゴール
メモ
モデルを使うアプリの主なリスクを挙げ、それぞれが自分の設計のどこにあるかを言うことができる。
重要語句
| ことば | いみ |
|---|---|
| 安全でない出力処理 | 返信を検査せずに、コードやマークアップとして使うこと |
| 過剰な権限 | タスクに必要な以上のことができるモデル |
| データ漏えい | 非公開の文章が、あってはならない場所へ届くこと |
| 過剰利用 | 要求やコストが、計画を超えて増えること |
| 信頼境界 | 外からの文章がプログラムに入ってくる線 |
学生が入力するセルは、このプレビューでは空白です。
どのリスクにも場所がある
設計を箱で描きます。箱ごとに、どんな文章が入り、その箱が何をできるかを問います。リスクとは、信頼できない文章が、力のある場所へ行くことです。
この種のアプリの主なリスクには、OWASP(オワスプ。Webアプリの安全を研究する非営利の団体)が公開している一覧があります。全体を知るために読んでください。このレッスンは、そのうち五つを私たちの言葉で扱います。
設計の中で、外からの文章がプログラムに入ってくる線を、信頼境界と呼びます。チャット欄、読み込むページ、返信を使う所は、どれもこの線の上にあります。リスクを探すときは、まずこの線を図に引いてください。
例: 五つのリスクと場所
| リスク | 潜む場所 | 作り話の例 | 最初の検査 |
|---|---|---|---|
| インジェクション | 文章がプロンプトに入る所 | ページがルールを無視せよと書く | データを分け、返信を検査する |
| 安全でない出力 | 返信を使う所 | scriptタグ入りの返信をページに置く | エスケープするか、厳密に解析する |
| 過剰な権限 | ツールが動く所 | フォルダを削除できるヘルプボット | 許可リストと承認 |
| データ漏えい | プロンプト、ログ、返信 | キーをプロンプトに貼りログに残す | 秘密と個人情報を入れない |
| 過剰利用 | モデルを呼ぶルート | 1人が大量の要求を送る | 利用者ごと、一日ごとの上限 |
学生が入力するセルは、このプレビューでは空白です。
返信は信頼できない入力
モデルは、攻撃者がデータに入れた文章を繰り返すことがあります。そのため、返信は見知らぬ人が入力したフォームの欄と同じに扱います。
ページに表示するなら、エスケープします。返信からクエリやコマンドを組み立ててはいけません。決まった形に解析して、欄ごとに確かめます。
モデル用の補助を書く(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"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
リスク表を書く(1)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '' '// データとして書いた小さな設計。各部品と、そこに入ってくるもの。' 'const design = [' ' { part: \"チャット欄\", input: \"入力された文章\" },' ' { part: \"文書の読み込み\", input: \"他の人が書いたページ\" },' ' { part: \"プロンプトの組み立て\", input: \"秘密のキーと文章\" },' ' { part: \"返信を表示するページ\", input: \"HTMLとして表示するモデルの文章\" },' ' { part: \"ツールの実行部\", input: \"行動を選ぶモデルの文章\" },' ' { part: \"APIのルート\", input: \"いくつでも届く要求\" },' '];' > risks.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
リスク表を書く(2)
docker exec -w /home/node/project agent sh -c "printf '%s\n' '// 私たち自身の表: 入力の種類、それがもたらすリスク、検査。' 'const rules = {' ' \"入力された文章\": [\"インジェクション\", \"データを分けて返信を検査する\"],' ' \"他の人が書いたページ\": [\"間接インジェクション\", \"データと示して返信を検査する\"],' ' \"秘密のキーと文章\": [\"データ漏えい\", \"秘密をプロンプトに入れない\"],' ' \"HTMLとして表示するモデルの文章\": [\"安全でない出力\", \"表示の前にエスケープする\"],' ' \"行動を選ぶモデルの文章\": [\"過剰な権限\", \"使えるツールを許可リストで絞る\"],' ' \"いくつでも届く要求\": [\"過剰利用\", \"利用者ごと、一日ごとに上限を置く\"],' '};' 'for (const item of design) {' ' const rule = rules[item.input];' ' console.log(item.part + \" -> \" + rule[0] + \"。検査: \" + rule[1] + \"。\");' '}' >> risks.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
リスク表を書く(3)
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function escapeHtml(text) {' ' const quote = String.fromCharCode(34);' ' return text.split(\"&\").join(\"&\").split(\"<\").join(\"<\")' ' .split(\">\").join(\">\").split(quote).join(\""\");' '}' 'async function main() {' ' const question = [{ role: \"user\", content: \"パン屋のWebサイトを訪れた人への、短い歓迎の一文を書いてください。\" }];' ' const reply = await ask(question, \"ようこそ、私たちのパン屋へ。ご来店をお待ちしています。\", 60);' ' const risky = \"こんにちは <b onmouseover=demo()>あなた</b>\";' ' console.log(\"作り話の危険な返信: \" + risky);' ' console.log(\"ページ用にエスケープ: \" + escapeHtml(risky));' ' console.log(\"モデルの返信をエスケープ: \" + escapeHtml(reply));' '}' 'main();' >> risks.js"ここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
リスク表を実行
docker exec -w /home/node/project agent node risks.jsここでは表示のみで、実行されません。無料アカウントを作成すると、実行ボタンで自分のラボのターミナルに入力できます。
出力の一例
| 行 | 意味 |
|---|---|
| チャット欄 -> インジェクション | 設計の各部品のリスクと、最初の検査(六行) |
| 記録済みサンプル | モデルが答えなかったときだけ、六行の後に一行出る |
| ページ用にエスケープ | 山かっこが、無害な文字になった |
| モデルの返信をエスケープ | 普通の返信はそのまま通る。言葉は実行のたびに変わる |
学生が入力するセルは、このプレビューでは空白です。
権限、秘密、コスト
過剰な権限は、ツールを足すたびに大きくなるリスクです。仕事ができる範囲で、いちばん小さな力だけをモデルに与えます。
データ漏えいには、攻撃者は要りません。プロンプトの中のキーや、ログの中の名前だけで起こります。過剰利用も同じで、ループや忙しい日が一か月分の予算を使い切ることがあります。
よくある間違い
注意
これらのリスクは、誰かが攻撃したときだけの問題だと考える人が多くいます。多くは偶然にも起こります。一晩中モデルを呼び続けるループや、顧客のメールを残すログです。攻撃者が届く箱だけでなく、設計のすべての箱を確かめてください。