0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LLMの返答を画面に出す前の、出力インジェクション対策チェックリスト【WordPressプラグイン】

0
Posted at

先に、いちばん大事な一行を書きます。LLMの返答は、ユーザーが打ち込んだ入力と同じくらい、信用できません。なのに多くの実装は、LLMの出力を「自前で作った安全なデータ」のように扱って、そのまま画面に流し込みます。ここが、出力インジェクションの入口です。

自分は、AIチャットボットのプラグインを作っていて、リリース後に35個のバグを踏みました。いちばん怖かったのは、入力側ではなく、出力側でした。この記事は、そのとき塞いだ観点を、実装チェックリストの形で残します。

そもそも、何が危ないのか

普通、開発者はユーザー入力を疑います。エスケープする、サニタイズする。でも、LLMの返答に対しては、その警戒がすっぽり抜けがちです。

理由はたぶん、LLMの出力が「自分のシステムの内側から来たもの」に見えるからです。実際には違います。LLMは、ユーザーのプロンプトや、外部から取り込んだ文書を材料に、文章を生成する。つまりLLMの返答は、外部の入力が形を変えて戻ってきたものです。信頼できる出どころ、ではありません。

自分はこれを「二重trust問題」と呼んでいます。ユーザー入力は疑うのに、それをLLMに通した"だけ"の出力を、なぜか信じてしまう。プロンプトインジェクションで仕込まれた指示が、返答の中に <script> として紛れていても、そのまま出力すれば動きます。

チェックリスト

上から順に、実装で確認する観点です。

1. LLMの返答を innerHTML に入れない

いちばんの定番です。返答をそのまま innerHTML に流すと、返答に含まれるタグが実行されます。

// ❌ NG:返答をそのままHTMLとして挿入
chatBox.innerHTML = response.text;
// ✅ OK:テキストとして入れる(タグは文字列のまま表示される)
chatBox.textContent = response.text;

マークダウンを表示したいなら、innerHTML 直挿しではなく、サニタイズを通したうえでHTMLにします。

// ✅ Markdownを出すなら、サニタイザを挟む(例:DOMPurify)
chatBox.innerHTML = DOMPurify.sanitize(marked.parse(response.text));

2. サーバー側でも、出力をエスケープする

フロントだけの対策は、片手落ちです。返答をPHP側でHTMLに埋め込む箇所や、REST経由で返す箇所でも、エスケープします。

// ❌ NG:返答をそのままHTMLに埋める
echo '<div class="msg">' . $llm_reply . '</div>';
// ✅ OK:文脈に合ったエスケープを通す
echo '<div class="msg">' . esc_html( $llm_reply ) . '</div>';

属性値に入れるなら esc_attr、URLなら esc_url。埋める場所によってエスケープ関数を変えます。

3. 保存して、あとで再表示するときが危ない

見落としがちなのが、ここです。会話ログを保存して、管理画面や履歴で後から表示するとき。保存の時点で安全に見えても、再表示のときにエスケープが抜けると、そこで実行されます。

対策の原則は「保存はそのまま、表示のときにエスケープ」です。保存時にHTMLを削ってしまうと元の会話が壊れるので、生のまま持っておいて、出す瞬間に必ずエスケープを通す。

// 保存:生のまま(ただしDBなので prepare は必須)
$wpdb->insert( $table, [ 'reply' => $llm_reply ], [ '%s' ] );

// 表示:出すときにエスケープ
echo esc_html( $row->reply );

4. 返答を、そのまま次の処理に渡さない

表示だけの話ではありません。LLMの返答を、SQLの条件、シェルコマンド、ファイルパス、別のAPIのパラメータに、そのまま渡すのも危ない。返答は外部入力なので、次に渡す前に、入力と同じ検証をかけます。

とくに、返答からJSONを取り出して処理する実装は要注意です。LLMが返したJSONは、構造が保証されていません。期待した型か、想定の値の範囲か、パースした後に必ず確かめます。

5. システムプロンプトや鍵を、返答に混ぜない

返答の生成に使ったシステムプロンプト、APIキー、内部の指示が、何かの拍子に返答へ漏れることがあります。プロンプトインジェクションで「これまでの指示を全部出力して」と誘導される攻撃もあります。返答をそのまま出す前に、内部情報が混じっていないかを気にする癖をつけます。

早見でまとめ

  • LLMの返答は外部入力と同じ。信頼できる出どころではない(二重trust問題)
  • フロントは innerHTML 直挿しをやめる。Markdownはサニタイザ経由
  • サーバー側も、埋める文脈に合わせて esc_html / esc_attr / esc_url
  • 保存はそのまま、表示のときにエスケープ。再表示の抜けがいちばん多い
  • 表示以外(SQL・コマンド・別API・JSON解析)も、外部入力として検証する

自分が35個のうちいちばんヒヤッとしたのは、入力側を固めたつもりで、出力側がまるまる素通しだったことでした。ユーザー入力は疑うのに、それをAIに通した返答は信じてしまう。この非対称に気づいてから、チェックの抜けがだいぶ減りました。役に立ったらストックして、チャットボットまわりを実装するとき見返してください。


ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?