旅行アプリ開発記録④ LLMを組み込んだアプリでのセキュリティ対応
LLMを利用するアプリケーションにおいて、入力から出力までを安全に処理するための多層防御の設計について解説します。この記事ではLLMに絞っていて、一般的なDOS対策等は別で必要です。
以下の4つの要素を中心に書きます。
- LLM応答の無害化
- プロンプトインジェクション検出
- 無害化+再検出
- 入力サニタイゼーション
※:あくまで今回のセキュリティ対応の一例として記載してます。セキュリティの観点で全てを書いてはいないですし、そのままのソフトではないです!笑
1. 入力サニタイゼーション
狙い
ユーザー入力から制御文字・不可視文字・危険記号を除去し、安全な文字列に変換します。
今回のアプリでもユーザー入力のテキストボックスをプロンプトに加える構成になっているので、鍵などを不正に変更したりされうるので、事前にチェックを行います。
実装概要
def sanitize_input(text: str) -> str:
cleaned = re.sub(r'[\x00-\x1F\x7F]', '', text)
cleaned = re.sub(r'[\u200B-\u200F\u202A-\u202E\u2060-\u206F]', '', cleaned)
cleaned = re.sub(r'[<>\[\]\\{}\*\%\$\@\=\;\'\"`]', '', cleaned)
return cleaned.strip()
注視点
-
制御文字の除去
ASCII制御文字(0x00–0x1F, 0x7F)は端末操作や改行注入に使われる恐れがあるため削除。 -
不可視文字の除去
ゼロ幅スペースや方向制御文字は、UI偽装や文字列比較すり抜けに悪用されるため除去。 -
危険記号の除去
HTMLタグ、コマンド注入、SQLインジェクションなどに使われやすい記号を事前に排除。 -
strip()による前後空白削除
見た目に気づきにくい空白や改行を除去し、後続処理の一致判定を安定化。
2. プロンプトインジェクション検出
狙い
ユーザーや外部ソースからの入力や、LLM応答内に含まれるメタ指令(例:「root」「admin」「削除」など)を検出します。
実装概要
def detect_prompt_injection(text: str, stage: str = "unknown") -> bool:
dangerous_patterns = [r"root", r"sudo",r"exec", ...]
for pattern in dangerous_patterns:
if re.search(pattern, text, re.IGNORECASE):
return True
return False
注視点
-
危険キーワードリストによる検出
OSコマンド、権限昇格、破壊的操作、セキュリティ回避などに関連する単語を事前定義。 -
大文字・小文字の区別を無効化
re.IGNORECASE により、表記ゆれ(Root, ROOT, root)にも対応。
3. LLM応答の無害化
狙い
LLMが返すテキスト中の「危険な指令語(sudo, exec, shell…)」をそのまま実行指示として解釈されない形に変換します。
本実装では、該当語を引用符で囲むことで弱毒化しています。
実装概要
def sanitize_llm_response(text: str) -> str:
dangerous_words = [r'\bsudo\b', r'\bexec\b', r'\bshell\b', ...]
sanitized_text = text
for pattern in dangerous_words:
sanitized_text = re.sub(pattern, r'"\\g<0>"', sanitized_text, flags=re.IGNORECASE)
return sanitized_text
注視点
-
直接実行の防止
危険な単語を引用符で囲み、シェルやスクリプト実行として扱われない形に変換。 -
正規表現での単語境界指定
\b により単語単位でマッチし、部分一致の誤検出を回避。 -
大文字小文字の無視
re.IGNORECASE で Root / ROOT / root のようなバリエーションにも対応。 -
実装コストが低い
簡易的かつ高速な処理で、応答後フィルタとして組み込みやすい。
4. 無害化+再検出
狙い
無害化した後にもう一度チェックを行い、本当に危険な要素だけを検出します。
検出時には無害化済みのテキストを返却して安全性を担保します。
実装概要
def detect_prompt_injection_with_sanitization(text: str, stage: str = "unknown") -> tuple[bool, str]:
sanitized_text = sanitize_llm_response(text)
dangerous_patterns = [r"APIキー", r"システムプロンプト", ...]
for pattern in dangerous_patterns:
if re.search(pattern, sanitized_text, re.IGNORECASE):
return True, sanitized_text
return False, sanitized_text
注視点
-
二段階防御
まず無害化、次に再検出することで、すり抜けや構造的改変を防ぐ。 -
重要語の優先チェック
無害化後でも残りうる「APIキー」「システム破壊」などを特別扱い。 -
誤検出の低減
無害化済みテキストを対象にすることで、引用や説明文への過剰反応を抑える。 -
安全な返却データ
検出時も無害化済み文字列を返すため、上位処理が安全に継続可能。
補足:なぜ応答データにも適用するのか
-
LLM経由の間接攻撃対策
攻撃者はユーザー入力ではなく、外部データや他システムからの情報を経由して危険指令を注入することがある -
信頼できない出力の原則
LLM出力は原則「外部から来たデータ」と同様に扱い、入力と同じレベルで検証が必要 -
後工程の安全性確保
生成テキストがそのまま別APIやシェルに渡る場合、危険語が実行コマンド化する恐れがある
多層防御の流れ
- 入力サニタイズ
- プロンプトインジェクション検出
- LLM応答の無害化
- 無害化後の再検出
- 返却直前の最終セキュリティチェック(構造検証+危険語チェック)
LLMが自由の利くアプリなので、他のAPIサービスより気を使う必要があると思います。勝手に認証情報などを応答されるとセキュリティホールになってしまうため、基本的な対策はすべて実施すべきと思います。
Discussion