入力バリデーションとサニタイズ

LLMプロンプトエンジニアリング

本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。

LLMにおける入力バリデーションとサニタイズのプロンプト設計

LLMにユーザー入力を処理させる際、セキュリティとデータ品質を確保するため入力バリデーションとサニタイズは不可欠です。本稿では、そのプロンプト設計、評価、改良のプロセスを詳述します。

ユースケース定義

本稿では、WebアプリケーションやAPIに送られるユーザーからの任意のテキスト入力に対し、LLMが以下の処理を行うシナリオを想定します。

  1. バリデーション: 入力がメールアドレス、数値(1~100)、日付(YYYY-MM-DD)のいずれかの指定された形式に合致するか検証します。
  2. サニタイズ: 入力に含まれる危険なHTMLタグやSQLキーワード、JavaScriptコード断片を除去し、安全な文字列に変換します。

制約付き仕様化(入出力契約)

入力契約

  • フォーマット: 任意のテキスト文字列。最大256文字。
  • エンコーディング: UTF-8。
  • 禁止事項: なし(LLMが処理するため)。

出力契約

  • フォーマット: JSON形式の文字列。{"status": "success", "type": "...", "value": "...", "message": "..."} または {"status": "error", "type": "...", "message": "..."}
  • 成功時の挙動:
    • status: “success”
    • type: “email”, “number”, “date”, “text” のいずれか。
    • value: バリデートされ、サニタイズされた入力値。
    • message: 処理結果に関する簡潔なメッセージ(例: “入力は有効です。”)。
  • 失敗時の挙動:
    • status: “error”
    • type: バリデーションエラーの種類、または “unknown”。
    • message: 具体的なエラー内容(例: “無効なメールアドレス形式です。”, “数値が範囲外です。”, “危険な文字列が検出されました。”)。
  • 禁止事項: 処理されていない危険な文字列(<script>DROP TABLEなど)の出力は厳禁です。出力されたJSON以外の形式も禁止します。

プロンプト設計

1. ゼロショットプロンプト

あなたは入力バリデーションとサニタイズを行うセキュリティアシスタントです。
以下のルールに従い、ユーザーからの入力を検証・サニタイズし、JSON形式で結果を返してください。

ルール:
1.  入力がメールアドレス形式(RFC 5322相当)であるか検証してください。
2.  入力が1~100の範囲の整数であるか検証してください。
3.  入力がYYYY-MM-DD形式の日付であるか検証してください。
4.  上記いずれにも該当しない場合は、一般テキストとして扱います。
5.  全ての入力に対し、`script`タグ、`onerror`属性、SQLキーワード(`SELECT`, `INSERT`, `UPDATE`, `DELETE`, `DROP TABLE`など)を検出・除去し、安全な文字列にしてください。
6.  結果はJSON形式で出力してください。
    -   成功時: `{"status": "success", "type": "email|number|date|text", "value": "安全な値", "message": "..."}`
    -   失敗時: `{"status": "error", "type": "email|number|date|text|unknown", "message": "エラー内容"}`

入力: {user_input}

2. 少数例プロンプト

あなたは入力バリデーションとサニタイズを行うセキュリティアシスタントです。
以下の例を参考に、ユーザーからの入力を検証・サニタイズし、JSON形式で結果を返してください。

---
入力: test@example.com
出力: {"status": "success", "type": "email", "value": "test@example.com", "message": "メールアドレスが有効です。"}
---
入力: <script>alert('XSS')</script>user@example.jp
出力: {"status": "success", "type": "email", "value": "user@example.jp", "message": "メールアドレスが有効で、危険な文字列を除去しました。"}
---
入力: 50
出力: {"status": "success", "type": "number", "value": "50", "message": "数値が有効です。"}
---
入力: 101
出力: {"status": "error", "type": "number", "message": "数値が範囲外です(1-100)。"}
---
入力: 2023-11-01
出力: {"status": "success", "type": "date", "value": "2023-11-01", "message": "日付が有効です。"}
---
入力: invalid_email
出力: {"status": "error", "type": "email", "message": "無効なメールアドレス形式です。"}
---
入力: DROP TABLE users;
出力: {"status": "error", "type": "text", "message": "危険なSQLキーワードが検出されました。"}
---

入力: {user_input}

3. Chain-of-Thought制約型プロンプト

あなたは入力バリデーションとサニタイズを行うセキュリティアシスタントです。
以下のステップバイステップの思考プロセスと出力形式を厳守し、ユーザーからの入力を処理してください。

### 思考プロセス
1.  **入力の分析**: ユーザー入力 `{user_input}` を受け取ります。
2.  **タイプ推定**: 入力が以下のいずれのタイプに最も近いかを判断します。
    -   メールアドレス(RFC 5322相当の正規表現 `^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$` で検証)
    -   数値(1~100の範囲の整数か検証)
    -   日付(YYYY-MM-DD形式の正規表現 `^\d{4}-\d{2}-\d{2}$` で検証)
    -   上記に該当しない場合は「text」とします。
3.  **バリデーション**: 推定されたタイプに基づき、厳密なルールでバリデーションを実行します。
    -   メールアドレス: 形式が不正ならエラー。
    -   数値: 形式が不正、または範囲外ならエラー。
    -   日付: 形式が不正ならエラー。
4.  **サニタイズ**: 以下の危険な文字列を完全に除去します。
    -   HTMLタグ(例: `<script>`, `<div>`)
    -   特定の属性(例: `onerror`, `onload`)
    -   SQLキーワード(例: `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `DROP TABLE`, `;`)
    -   サニタイズ後に値が空になる場合、または危険な文字列が除去できない場合はエラーとします。
5.  **結果の生成**: バリデーションとサニタイズの結果をJSON形式で出力します。

### 出力フォーマット
必ず以下のJSON形式で出力してください。
成功時: `{"status": "success", "type": "email|number|date|text", "value": "安全な値", "message": "処理結果"}`
失敗時: `{"status": "error", "type": "email|number|date|text|unknown", "message": "エラー内容"}`

入力: {user_input}

評価

評価シナリオ

カテゴリ 入力値 期待される出力JSON
正例 test@example.com {"status": "success", "type": "email", "value": "test@example.com", "message": "..."}
正例 50 {"status": "success", "type": "number", "value": "50", "message": "..."}
正例 2023-10-26 {"status": "success", "type": "date", "value": "2023-10-26", "message": "..."}
難例 <script>alert(1)</script>safe@domain.com {"status": "success", "type": "email", "value": "safe@domain.com", "message": "..."}
難例 SELECT * FROM users; 10 {"status": "success", "type": "number", "value": "10", "message": "..."}
難例 101 {"status": "error", "type": "number", "message": "数値が範囲外です(1-100)。"}
難例 invalid-email {"status": "error", "type": "email", "message": "無効なメールアドレス形式です。"}
コーナーケース (空文字列) {"status": "error", "type": "unknown", "message": "入力が空です。"}
コーナーケース null {"status": "error", "type": "unknown", "message": "入力が空、または無効です。"}
コーナーケース user@.com {"status": "error", "type": "email", "message": "無効なメールアドレス形式です。"}

自動評価擬似コード

import re
import json

def evaluate_llm_output(llm_output_json_str: str, expected_output_json: dict) -> int:
    try:
        actual_output = json.loads(llm_output_json_str)
    except json.JSONDecodeError:
        return 0 # JSON形式でない場合は0点

    score = 0
    # 1. JSON形式の妥当性 (既にチェック済み)
    # 2. statusの一致
    if actual_output.get("status") == expected_output_json.get("status"):
        score += 1

    # 3. typeの一致
    if actual_output.get("type") == expected_output_json.get("type"):
        score += 1

    # 4. success時のvalueの一致とサニタイズチェック
    if actual_output.get("status") == "success":
        if actual_output.get("value") == expected_output_json.get("value"):
            score += 1
        # サニタイズの観点: 危険な文字列が含まれていないかチェック
        dangerous_patterns = [r"<script.*?>", r"onerror", r"SELECT", r"DROP TABLE"]
        is_sanitized = all(not re.search(p, actual_output.get("value", ""), re.IGNORECASE) for p in dangerous_patterns)
        if is_sanitized:
            score += 1

    # 5. error時のmessage内容の近似性 (部分一致でも可)
    elif actual_output.get("status") == "error":
        expected_message = expected_output_json.get("message", "")
        actual_message = actual_output.get("message", "")
        if expected_message in actual_message or actual_message in expected_message:
            score += 2 # エラーメッセージの重要度を高く設定

    return score

# 採点ルーブリック: 
# 0点: 全く異なる、JSON形式でない、危険な文字列を含む
# 1-3点: 部分的に正しい
# 4点: 完全に正しい

誤り分析

失敗モード

  1. 幻覚/誤ったバリデーション:
    • 無効な入力を有効と判断する(例: user@.comを有効なメールアドレスと認識)。
    • 有効な入力を無効と判断する(例: 99を範囲外と認識)。
  2. 様式崩れ:
    • 出力が指定のJSON形式ではない(例: プレーンテキスト、部分的なJSON)。
    • statustypeフィールドが期待値と異なる文字列になる。
  3. 脱線/禁止事項の漏れ:
    • サニタイズされるべき危険な文字列(XSSペイロード、SQLインジェクション)がvalueフィールドにそのまま残る。
    • サニタイズ後のvalueが、元の意図と大きく異なる(過剰な除去)。

抑制手法

  1. System指示の強化:
    • 正規表現パターンをプロンプト内に明示的に記述し、バリデーションロジックをLLMに厳密に指示します。
    • サニタイズすべき危険なキーワードやパターンリストを具体的に列挙し、除去を命令します。
  2. 検証ステップの導入 (CoT):
    • Chain-of-Thoughtプロンプトで、タイプ推定→バリデーション→サニタイズの各ステップを明確に指示し、LLMにその思考過程を出力させることで、内部でのロジック適用を促します。
  3. リトライ戦略:
    • LLMからの出力がJSON形式ではない、またはサニタイズが不十分な場合、再試行を促すプロンプトを追加し、再度処理を要求します。
    • 外部の正規表現ライブラリやサニタイザー関数を呼び出すためのファンクションコール機能を検討します。

改良

評価結果に基づき、失敗モードを抑制するためプロンプトを改良します。特に、Chain-of-Thought制約型プロンプトに正規表現パターンを直接記述し、サニタイズ対象キーワードリストをさらに詳細化することが有効です。また、JSON Schemaをプロンプトに含めることで、様式崩れのリスクを低減します。

再評価

改良されたプロンプトを用いて、同じ評価シナリオで再度LLMの出力を評価します。これにより、改良が有効であったか、新たな問題が発生していないかを確認します。自動評価スクリプトのスコア向上を目指します。

まとめ

LLMを用いた入力バリデーションとサニタイズは、適切なプロンプト設計により実現可能です。ゼロショットからChain-of-Thoughtへと段階的に制約を強め、詳細な入出力契約と厳密な評価基準を設けることで、セキュリティとデータ品質を確保できます。特に、危険な文字列の具体的なリスト化とJSON出力の厳格な指示が鍵となります。

プロンプト→モデル→評価→改良のループ

graph TD
    A["要求定義・要件分析"] --> B{"プロンプト設計"};
    B --> C["プロンプト生成"];
    C --> D[LLM];
    D --> E["出力結果"];
    E --> F{"評価"};
    F --|評価結果が不十分| G["誤り分析"];
    G --> H["改良案立案"];
    H --> B;
    F --|評価結果が良好| I["デプロイ/採用"];
ライセンス:本記事のテキスト/コードは特記なき限り CC BY 4.0 です。引用の際は出典URL(本ページ)を明記してください。
利用ポリシー もご参照ください。

コメント

タイトルとURLをコピーしました