TT Lab
ブログ はじめる
学ぶ 学習パス コース

LLMアプリのセキュリティ — インジェクションから出力処理まで

秘密と個人情報が漏れる場所 — 入れない、ふるい分ける、マスクする、検知する

TT Labで続きを見る

目標

カスタマーサポートボットで、システムプロンプトのキー、原文のままのログ、顧客の区別のない検索という3つの出口を見つけて塞ぎ、カナリアでプロンプトの漏えいを検知します。

なぜ重要か

モデルの入力に入ったものはいつか回答として出てくる可能性があり、入力を記録するログはその内容をより多くの場所へ複製します。「モデルに話さないよう書いておく」ことは境界ではありません — 本物のモデルは確率的なので、ほとんどの場合は守っても、ある日守らなくなります。このラボでは、秘密をそもそも入れず(ツール層)、他人のものは入れる前にフィルタリングし(検索)、ログはマスクし、それでも漏れたらカナリアで気づく、という順序で直します。採点ツールは採点のたびに新しいキー・カナリア・電話番号を作った一時データでアプリを起動し、その値が応答・ログのどこに残るかを確認します。

ステップ

  1. python3 /opt/fixtures/llmsec/kit.py support で素材を展開したあと、/root/llmsec/support/app.py を127.0.0.1:8102で起動しておいてください(0.0.0.0は禁止、ターミナルを閉じても動き続けるように)。curl 127.0.0.1:8102/health のseedが、展開時に受け取ったシードと同じでなければなりません。
  2. 昨日のログ /root/llmsec/support/logs/app-2026-11-04.log に原文のまま残ったものを数え、/root/llmsec/support/exposure.txt に tokens=<count>(ログに出てきた異なるユーザートークンの数)、phones=<count>(異なる電話番号の数)、key_lines=<count>(パートナー企業のキー API 키: pk-… を含む行の数)の3行を書いてください。
  3. build_system・run_tool を直して、パートナー企業のキーをシステムプロンプトから外し、注文照会ツールが実行されるときにサーバーが config.json から直接読むようにしてください。システムプロンプトを出すよう求めるリクエストに対しても、キーが応答・ログのどこにも出ず、注文照会はそのまま動作しなければなりません。
  4. log_request を直して、data/app.log にリクエストごとに1行(rid・ユーザー名)は残しつつ、X-Tokenの値・電話番号・メールアドレス・パートナー企業のキーが原文のまま残らないようにしてください。電話番号は末尾4桁だけを残します(***-****-1234)。
  5. build_system・check_output を直して、config.json の canary の値をシステムプロンプトに入れ、応答にその値が現れたら回答を丸ごとブロックし、data/audit.log にそのリクエストの rid と "event": "canary" を含むJSON行を残してください。正常な回答はそのまま出なければなりません。
  6. find_tickets を直して、モデルに入れるチケットをリクエストした顧客のものだけにフィルタリングしてください。他人のチケットの単語で尋ねても他人の情報が回答に混ざらず、自分のチケットについては答えなければなりません。
  7. incident ls-secret-leak を実行するとインシデントが発生します。顧客3人の電話番号が社外で見つかりました(/srv/ls-support/found.txt)。顧客ごとに経路を見つけて発生源を片付け、コードをラボのステップ3–6のように直したあと、/root/incident/ls-secret-leak/report.txt に <customer>: <code> の3行と evidence: <log line> を書いてください。コードは cross-tenant(他の顧客のチケットが回答に混ざる)、log-bundle(パートナー企業に送った診断バンドル)、misfiled-ticket(所有者が誤って記録されたチケット)、prompt-extract(システムプロンプトの抽出)のいずれかです。

参考

アプリを展開してループバックで起動する

python3 /opt/fixtures/llmsec/kit.py support で素材を展開したあと、/root/llmsec/support/app.py を127.0.0.1:8102で起動しておいてください(0.0.0.0は禁止、ターミナルを閉じても動き続けるように)。curl 127.0.0.1:8102/health のseedが、展開時に受け取ったシードと同じでなければなりません。

方向: ラボのアプリを外部に開く理由はありません — リッスンするアドレスがそのまま露出範囲です。 ツール: ss -ltnp、curl、setsid・nohup。 具体: 展開コマンドは上記のとおりです。起動後、ss -ltn で8102番が127.0.0.1として表示されるか、/healthのseedが /root/llmsec/support/data/seed.txt と同じかを確認してください。

昨日のログに原文のまま残ったものを数える

昨日のログ /root/llmsec/support/logs/app-2026-11-04.log に原文のまま残ったものを数え、/root/llmsec/support/exposure.txt に tokens=<count>(ログに出てきた異なるユーザートークンの数)、phones=<count>(異なる電話番号の数)、key_lines=<count>(パートナー企業のキー API 키: pk-… を含む行の数)の3行を書いてください。

方向: 原文のままのログは、プロンプトに入ったすべてのもののコピーです。何がいくつ複製されたかを数えることが、被害範囲を確定する第一歩です。 ツール: grep -o、sort -u、wc -l、jq、data/users.json。 具体: トークンはusers.jsonの値がログに現れるかで、電話番号は異なる値の数で、キーは行数で数えます — 3つは数える単位が異なります。

キーをシステムプロンプトからツール層へ

build_system・run_tool を直して、パートナー企業のキーをシステムプロンプトから外し、注文照会ツールが実行されるときにサーバーが config.json から直接読むようにしてください。システムプロンプトを出すよう求めるリクエストに対しても、キーが応答・ログのどこにも出ず、注文照会はそのまま動作しなければなりません。

方向: モデルにキーは必要ありません。キーが必要なのはツールを実行するサーバーコードです。 ツール: build_system、run_tool、jload("config.json")。 具体: システムプロンプトからキーの行をなくし、run_toolがシステムプロンプトを探る代わりに設定を読むように変えてください。採点ツールはキーを新しく作り、応答とconfig.json以外のすべてのファイルを確認します。

ログは残しつつマスクする

log_request を直して、data/app.log にリクエストごとに1行(rid・ユーザー名)は残しつつ、X-Tokenの値・電話番号・メールアドレス・パートナー企業のキーが原文のまま残らないようにしてください。電話番号は末尾4桁だけを残します(***-****-1234)。

方向: 止めれば調査できず、すべて記録すればログが漏えい経路になります。何を残すかを選ぶ作業です。 ツール: re.sub、json.dumps。 具体: ヘッダーは丸ごと残さず身元は名前で、質問・コンテキスト・回答の番号とメールアドレスは正規表現でマスクしてください。システムプロンプトは記録する理由がありません。

カナリアでプロンプトの漏えいを検知する

build_system・check_output を直して、config.json の canary の値をシステムプロンプトに入れ、応答にその値が現れたら回答を丸ごとブロックし、data/audit.log にそのリクエストの rid と "event": "canary" を含むJSON行を残してください。正常な回答はそのまま出なければなりません。

方向: カナリアは秘密の代わりに入れておくマーカーです。それが出力に現れたらシステムプロンプトが漏れたということなので、カナリアだけを消さずに回答を丸ごとブロックします。 ツール: build_system、check_output、audit()。 具体: カナリアは毎回設定から読み、比較の前に空白・大文字小文字をそろえてください。本物のモデルなら言い換えてすり抜けられるという限界も覚えておきます。

他人のチケットは入れる前にフィルタリングする

find_tickets を直して、モデルに入れるチケットをリクエストした顧客のものだけにフィルタリングしてください。他人のチケットの単語で尋ねても他人の情報が回答に混ざらず、自分のチケットについては答えなければなりません。

方向: 入力に入ったものは、話しうるものです。権限は検索の段階でかけます。 ツール: find_ticketsのuser、tickets.jsonのowner。 具体: スコアを付ける前に、所有者が異なるチケットを除いてください。

インシデント — 3人の顧客の番号が外部で見つかった

incident ls-secret-leak を実行するとインシデントが発生します。顧客3人の電話番号が社外で見つかりました — 見つかった断片は /srv/ls-support/found.txt、ボットは /srv/ls-support/app.py、データ・ログは /srv/ls-support/data/、先週パートナー企業に送った診断バンドルは /srv/ls-support/support-bundle.log です。顧客ごとに番号が漏れた経路を見つけて発生源を片付けてください — 誤って記録されたチケットの所有者は番号の所有者に正し、システムプロンプトの番号は外し、診断バンドルは行を残したまま番号だけをマスクします。コード /srv/ls-support/app.py は、ラボのステップ3–6で直した欠陥をすべて防がなければなりません(キーはツール層、ログはマスク、カナリア、検索は顧客ごと — 採点ツールがその攻撃を再び送ります)。/root/incident/ls-secret-leak/report.txt に <customer>: <code> の3行と evidence: <log line> を書きます。コードは顧客ごとに次のいずれかです: cross-tenant(他の顧客のチケットが回答に混ざる)、log-bundle(パートナー企業に送った診断バンドル)、misfiled-ticket(所有者が誤って記録されたチケット)、prompt-extract(システムプロンプトの抽出)。

方向: 症状は3つとも「番号が外部にある」です。見つかった断片の形そのものが、どのログから出てきえたかを示し、それが経路です。 ツール: grep -F、jq、tickets.jsonのowner、users.jsonのphone。 具体: 断片をそのままgrepして、app.logの回答(answer)なのか、診断バンドルの質問(q)なのかを確認してください。回答なら、そのリクエストをした人とコンテキストに入ったチケットの所有者を、番号の実際の所有者(users.json)と突き合わせると、他の顧客のチケットが混ざったケースとチケットの所有者が誤って記録されたケースが見分けられます。