April 17, 2026

Discordチャンネルへの内部ペイロード漏えい:ユーザーメッセージにおけるEXTERNAL_UNTRUSTED_CONTENTラッパー

内部ラッパーマーカーと不正な添付ファイル抽出テキストが、送信前にサニタイズされる代わりにDiscordチャンネルに転送されています。

🔍 症状

ユーザーが直接確認できるエラー

Discordでアシスタントとやり取りする際に、プレゼンテーション層に決して表示されるべきではない、生の内部コンテンツを含むメッセージがユーザーに見えてしまう問題が発生します。漏洩したコンテンツは2つの異なるパターンで表示されます:

パターン1: ラッパ構文の漏洩

シリアライズマーカーの生データを含むメッセージがDiscordチャットに直接表示されます:

<<<EXTERNAL_UNTRUSTED_CONTENT id="msg_abc123">>>
Source: External
UNTRUSTED Discord message body
<<<END_EXTERNAL_UNTRUSTED_CONTENT id="msg_abc123">>>

パターン2: 壊れた添付ファイルペイロードのスパム

繰り返される技術用語で構成された、無意味なテキストの長いブロック:

attach attachment attachment hookup toggle compiler 
attachment hookup toggle compiler attach attachment 
UNTRUSTED Discord message body Source External Source External
attach attachment attachment hookup toggle compiler

技術的な症状

コンポーネント症状
Discord Transport生のラッパータグがアウトバウンドメッセージペイロードに表示される
Attachment Handler壊れた抽出結果がチャンネルに転送される
Async Tool Completionキューに入れられた完了テキストに内部マーカーが含まれる
Sanitization Layerコンテキストとレンダリング間の境界適用が失敗する

トリガー条件

以下の操作のいずれかの後に問題が発生します:

  • アシスタントが添付ファイルを含むメッセージを処理した場合
  • Asyncツールの完了結果がDiscordチャンネルに配信された場合
  • 外部コンテンツがEXTERNAL_UNTRUSTED_CONTENTラッパーシステムを介して処理された場合
  • ファイル/画像添付ファイルを含むマルチターン会話の場合

🧠 原因

アーキテクチャの失敗ポイント

この漏洩は、内部処理とDiscordトランスポート間のメッセージパイプラインにおけるサニタイズ境界の失敗を示しています。OpenClawフレームワークは、エージェント処理中に信頼できないユーザーコンテンツを隔離するためにEXTERNAL_UNTRUSTED_CONTENTラッパーを使用します。このラッパーは次のことを行うべきです:

  1. コンテキストアセンブリ中に内部で消費される
  2. アウトバウンドトランスポート層にシリアライズされない
  3. メッセージがレンダリングパイプラインに到達する前にすべて削除される

失敗シーケンス

┌─────────────────────────────────────────────────────────────────┐
│                    MESSAGE FLOW (FAILING)                        │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Discord Message Received                                        │
│         │                                                        │
│         ▼                                                        │
│  ┌─────────────────┐                                            │
│  │ Content Wrapper │  ← EXTERNAL_UNTRUSTED_CONTENT added        │
│  │   Injection     │     to isolate untrusted input              │
│  └────────┬────────┘                                            │
│           │                                                      │
│           ▼                                                      │
│  ┌─────────────────┐                                            │
│  │  Agent Runtime   │  ← Wrapper consumed in context              │
│  │   Processing     │     (intended behavior)                    │
│  └────────┬────────┘                                            │
│           │                                                      │
│           ▼                                                      │
│  ┌─────────────────┐                                            │
│  │ Discord Transport│ ← SANITIZATION FAILURE                     │
│  │   Renderer       │   Wrapper not stripped before posting       │
│  └────────┬────────┘                                            │
│           │                                                      │
│           ▼                                                      │
│  ┌─────────────────┐                                            │
│  │  RAW WRAPPER +  │  ← User sees:                               │
│  │   Payload        │     <<>>   │
│  │   Forwarded      │     UNTRUSTED Discord message body          │
│  └─────────────────┘                                            │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

コードパスの分析

欠陥は、レスポンスメッセージが構築されるDiscordトランスポートアダプターにあります。期待されるコードパス:

// CORRECT FLOW (Expected)
function buildDiscordMessage(agentResponse) {
    const sanitized = sanitize(すべての内部マークアップを剥离);
    const message = createDiscordEmbed(sanitized);
    return message;
}

// ACTUAL FLOW (Defective)
function buildDiscordMessage(agentResponse) {
    // Sanitization missing or ineffective
    const message = createDiscordEmbed(agentResponse.raw);
    // Raw EXTERNAL_UNTRUSTED_CONTENT markers included
    return message;
}

添付ファイルペイロードの破損

「ガベージテキスト」パターンは、添付ファイルテキスト抽出で次が発生した結果です:

  1. バイナリまたは壊れた添付ファイルデータが処理される
  2. 抽出により壊れたUnicode/コードポイントシーケンスが生成される
  3. マルチ添付ファイル処理中にこれらのシーケンスが繰り返される
  4. 壊れたペイロードがコンテンツフィルタリングをバイパスする

サブシステムの責任

サブシステム期待される動作実際の動作
DiscordTransport投稿前に内部ラッパーを削除生コンテンツを転送
ContentSanitizerEXTERNAL_*マーカーを削除フィルターが無効またはバイパス済み
AttachmentHandlerクリーンな抽出テキスト壊れたペイロードを転送
AsyncCompletionRouterクリーンな完了を配信デバッグマーカーを含む

🛠️ 解決手順

フェーズ1: Discord Transportでのラッパー伝播を無効化

ファイル: src/transports/discord/index.ts(または同等のトランスポートモジュール)

修正前(欠陥あり):

async function handleAssistantMessage(message: ProcessedMessage): Promise<void> {
    const discordMessage = {
        content: message.content,
        embeds: message.embeds
    };
    await this.client.sendMessage(discordMessage);
}

修正後(修正済み):

async function handleAssistantMessage(message: ProcessedMessage): Promise<void> {
    const sanitizedContent = this.sanitizeForDiscord(message.content);
    const discordMessage = {
        content: sanitizedContent,
        embeds: message.embeds
    };
    await this.client.sendMessage(discordMessage);
}

private sanitizeForDiscord(content: string): string {
    // Remove all internal wrapper markers
    const patterns = [
        /<<<EXTERNAL_UNTRUSTED_CONTENT[^>]*>>>/gi,
        /<<<END_EXTERNAL_UNTRUSTED_CONTENT[^>]*>>>/gi,
        /<<<INTERNAL_[A-Z_]+>>>/gi,
        /Source:\s*(External|Internal)/gi
    ];
    
    let sanitized = content;
    for (const pattern of patterns) {
        sanitized = sanitized.replace(pattern, '');
    }
    
    return sanitized.trim();
}

フェーズ2: 添付ファイル抽出のサニタイズを強化

ファイル: src/handlers/attachment-extractor.ts

修正前(欠陥あり):

function extractTextFromAttachment(attachment: Attachment): string {
    const raw = processAttachmentBinary(attachment);
    return raw.text || '';
}

修正後(修正済み):

function extractTextFromAttachment(attachment: Attachment): string {
    const raw = processAttachmentBinary(attachment);
    let text = raw.text || '';
    
    // Discard malformed extractions (repeated tokens indicate corruption)
    if (isMalformedExtraction(text)) {
        console.warn(`[Sanitizer] Discarding malformed attachment extraction for ${attachment.id}`);
        return '';
    }
    
    // Strip any internal markers that slipped through
    text = stripInternalMarkers(text);
    
    // Limit length to prevent spam
    const MAX_LENGTH = 4000;
    if (text.length > MAX_LENGTH) {
        text = text.substring(0, MAX_LENGTH) + '\n[Attachment content truncated]';
    }
    
    return text;
}

function isMalformedExtraction(text: string): boolean {
    // Detect repeated token patterns indicating extraction failure
    const tokens = text.toLowerCase().split(/\s+/);
    const uniqueRatio = new Set(tokens).size / tokens.length;
    
    // If <20% unique tokens, extraction is likely corrupted
    return uniqueRatio < 0.2 && tokens.length > 50;
}

フェーズ3: Asyncツール完了ルーティングの修正

ファイル: src/routing/async-completion-router.ts

修正前(欠陥あり):

async function forwardCompletion(result: ToolResult): Promise<void> {
    const message = buildChannelMessage(result);
    await this.transport.post(message);
}

修正後(修正済み):

async function forwardCompletion(result: ToolResult): Promise<void> {
    // Ensure clean payload before routing
    const cleanPayload = this.sanitizer.sanitize(result.payload);
    
    if (cleanPayload.isDirty) {
        console.error('[Router] Sanitizer detected dirty payload in async completion');
        // Log for debugging, but still deliver cleaned content
    }
    
    const message = buildChannelMessage({
        ...result,
        payload: cleanPayload.content
    });
    
    await this.transport.post(message);
}

フェーズ4: トランスポートレベルのガードを追加

ファイル: src/transports/discord/client.ts

Discord API呼び出し前に最終的なサニタイズゲートを追加します:

async sendMessage(message: DiscordMessage): Promise<API.Message> {
    // Final safety net - ensure no internal content escapes
    const finalContent = this.stripInternalMarkers(message.content);
    
    if (finalContent !== message.content) {
        logger.warn('[DiscordTransport] Stripped internal markers before send');
    }
    
    // Hard block if wrapper syntax detected (indicates serious leak)
    if (this.containsWrapperSyntax(finalContent)) {
        logger.error('[DiscordTransport] CRITICAL: Wrapper syntax detected at send time');
        throw new Error('SANITATION_FAILURE: Internal content detected in outbound message');
    }
    
    return this.api.createMessage(this.channelId, {
        content: finalContent,
        embeds: message.embeds
    });
}

private containsWrapperSyntax(text: string): boolean {
    return /<<<[A-Z_]+>>>/.test(text);
}

🧪 検証

テストケース1: ラッパーマーカーの剥离

既知の内部コンテンツに対してサニタイズ関数を実行します:

const { sanitizeForDiscord } = require('./src/transports/discord/sanitizer');

const testCases = [
    {
        input: '<<>>UNTRUSTED Discord message body<<>>',
        expected: 'UNTRUSTED Discord message body'
    },
    {
        input: 'Source: External\nUser message\nSource: Internal',
        expected: 'User message'
    },
    {
        input: '<<>>\nValid response\n<<>>',
        expected: 'Valid response'
    }
];

let passed = 0;
for (const { input, expected } of testCases) {
    const result = sanitizeForDiscord(input);
    if (result === expected) {
        console.log('✅ PASS:', JSON.stringify(result));
        passed++;
    } else {
        console.log('❌ FAIL:', JSON.stringify({ input, expected, got: result }));
    }
}

console.log(`\nResults: ${passed}/${testCases.length} tests passed`);
process.exit(passed === testCases.length ? 0 : 1);

期待される出力:

✅ PASS: "UNTRUSTED Discord message body"
✅ PASS: "User message"
✅ PASS: "Valid response"

Results: 3/3 tests passed

テストケース2: エンドツーエンドのDiscord Transportテスト

// Integration test - requires mock Discord client
const { DiscordTransport } = require('./src/transports/discord');

const mockClient = {
    messages: [],
    async sendMessage(msg) {
        this.messages.push(msg);
        return { id: 'test-' + Date.now() };
    }
};

const transport = new DiscordTransport(mockClient);

// Simulate message with internal markers
const dirtyMessage = {
    content: '<<>>Corrupted payload<<>>',
    embeds: []
};

try {
    await transport.handleAssistantMessage(dirtyMessage);
    const sent = mockClient.messages[0];
    
    if (sent.content.includes('<<<')) {
        console.log('❌ FAIL: Wrapper syntax leaked to Discord');
        console.log('Sent content:', sent.content);
        process.exit(1);
    }
    
    console.log('✅ PASS: Message sanitized before Discord send');
    console.log('Final content:', sent.content);
} catch (e) {
    if (e.message.includes('SANITATION_FAILURE')) {
        console.log('✅ PASS: Hard block triggered on dirty content');
    } else {
        throw e;
    }
}

テストケース3: 壊れた添付ファイルの検出

const { isMalformedExtraction } = require('./src/handlers/attachment-extractor');

// Corrupted payload (high repetition)
const corrupted = Array(200).fill('attach attachment hookup toggle compiler').join(' ');
console.log('Corrupted detection:', isMalformedExtraction(corrupted)); // Should be true

// Valid text
const valid = 'User uploaded a document containing meeting notes from Tuesday.';
console.log('Valid detection:', isMalformedExtraction(valid)); // Should be false

期待される出力:

Corrupted detection: true
Valid detection: false

検証チェックリスト

修正を適用した後、以下を確認してください:

  • Discordメッセージ履歴に<<<EXTERNAL_UNTRUSTED_CONTENT文字列がない
  • Discordメッセージ履歴に<<<END_EXTERNAL_UNTRUSTED_CONTENT文字列がない
  • Source: External / Source: Internalがユーザーに表示されるメッセージに表示されていない
  • 添付ファイルから抽出されたテキストに繰り返しトークンパターンがない(20%未満のユニーク率)
  • sanitizeForDiscord関数のユニットテストが成功する
  • Discord transportの統合テストが成功する
  • 送信時にラッパー構文が検出された場合、ハードブロックがエラーをスローする

⚠️ よくある落とし穴

環境固有のトラップ

Dockerコンテナ隔離

OpenClawをDockerで実行している場合、サニタイズモジュールが適切にマウントされており、バグのあるバージョンに戻すボリュームに上書きされていないことを確認してください:

# Wrong - local source overrides container
docker run -v $(pwd)/src:/app/src openclaw:latest

# Correct - use container's fixed source
docker run openclaw:latest

Windowsの改行コード

コンテンツが\r\n改行を含む場合、ラッパーの正規表現が失敗する可能性があります。サニタイズが両方を処理することを確認してください:

// BROKEN: Only matches Unix line endings
const pattern = /<<<EXTERNAL_UNTRUSTED_CONTENT[^>]*>>>/g;

// FIXED: Handles both Windows and Unix
const pattern = /<<<EXTERNAL_UNTRUSTED_CONTENT[^>\r\n]*>>>/gi;

Node.jsバージョンの非互換性

ユニーク比率計算のためのSetコンストラクタはNode.js 12+が必要です。互換性を確認してください:

// Feature detection fallback
const uniqueRatio = typeof Set !== 'undefined' 
    ? new Set(tokens).size / tokens.length 
    : [...new Set(tokens)].length / tokens.length;

設定の落とし穴

環境変数でサニタイズが無効になっている

一部のデプロイメントではデバッグ用にサニタイズが無効になっており、これによりこの漏洩が発生します:

# .env file - ensure sanitization is NOT disabled
SANITIZATION_ENABLED=true
# SANITIZATION_ENABLED=false  ← REMOVE OR SET TO TRUE

トランスポート設定がベースのサニタイザーを継承していない

カスタムDiscord transport実装を使用している場合、ベースのContentSanitizerを継承していることを確認してください:

// WRONG: Custom transport bypasses sanitization
class DiscordTransportCustom {
    async send(msg) { /* direct send without sanitization */ }
}

// CORRECT: Inherit sanitization
class DiscordTransportCustom extends BaseTransport {
    async send(msg) {
        return super.send(this.sanitizer.sanitize(msg));
    }
}

ランタイムのエッジケース

Unicode正規化攻撃

悪意のあるコンテンツは、パターンマッチングをバイパスするためにUnicodeに見た目が似た文字を使用する場合があります:

// Attempted bypass: Cyrillic 'а' instead of Latin 'a'
const malicious = '<<<ЕXTERNAL_UNTRUSTED_CONTENT id="1">>>'; // Different chars

// Defensive: Normalize before pattern matching
const normalized = content.normalize('NFKC');
const sanitized = stripInternalMarkers(normalized);

同時メッセージサニタイズの競合状態

複数の非同期ツール完了が同時に発火する場合:

// Ensure thread-safe sanitization by not mutating shared state
// WRONG: Mutates input in place
function sanitize(content) {
    content = content.replace(pattern1, '');
    return content.replace(pattern2, ''); // Returns mutated original
}

// CORRECT: Immutable operations
function sanitize(content) {
    return content
        .replace(pattern1, '')
        .replace(pattern2, '');
}

空のサニタイズ結果

サニタイズがすべてのコンテンツを剥离した場合、メッセージが送信されないことを確認してください(空のスパムを避けるため):

const sanitized = stripInternalMarkers(raw);
if (!sanitized.trim()) {
    logger.warn('[Discord] Sanitization produced empty message, discarding');
    return; // Do not post to Discord
}

🔗 関連するエラー

直接関連する問題

エラー/問題説明関連性
EXTERNAL_UNTRUSTED_CONTENTラッパーの漏洩生の内部マーカーがユーザーに表示される主要問題 - 同一の症状
添付ファイルテキスト抽出の破損添付ファイルからのガベージ/壊れたテキスト同一の原因:サニタイズ境界の欠落
非同期ツール完了のスパムチャンネル内の重複/壊れた完了トランスポートレンダリング欠陥を共有
Discordレート制限エラー漏洩によりメッセージスパムループが発生した場合に発生の可能性ガベージコンテンツからの二次的症状
メッセージキューのバックアップ汚いコンテンツでトランスポートが繰り返し失敗する場合サニタイズされていない入力の結果

歴史的に関連する問題

Issue IDタイトル関連性
GH-XXXAsync完了ペイロードにサニタイザーが適用されていない直接的な前身 - 修正がすべてのパスに伝播されていない
GH-YYYDiscord transportが開発モードでコンテンツフィルタリングをバイパス境界失敗の環境固有の亜種
GH-ZZZ添付ファイル抽出がバイナリガベージを返している同一の破損メカニズム、異なるサブシステム
GH-AAAログに内部ラッパー構文が表示されるコードベース全体へのラッパーの拡散を示唆

エラーコードリファレンス

コード意味修正関連性
DISCORD_TRANSPORT_001メッセージが2000文字制限を超えているサニタイズは失敗するのではなくトランケートする必要がある
DISCORD_TRANSPORT_002アウトバウンドメッセージでのサニタイズ失敗ハードブロックは深刻な漏洩を示す
CONTENT_SANITIZE_001入力でのパターンマッチング失敗正規表現の脆弱性がバイパスを許可
ATTACHMENT_EXTRACT_001バイナリ抽出がテキスト以外を生成壊れたペイロードを破棄し、転送しない
ASYNC_COMPLETION_001キューで汚いペイロードが検出された配信前サニタイズが欠落

関連する設定パラメータ

パラメータ場所デフォルトセキュリティ影響
SANITIZATION_ENABLED環境truefalseの場合、すべてのサニタイズがバイパスされる
DISCORD_STRICT_MODE設定falsetrueの場合、ラッパー検出時にハードブロックを有効化
ATTACHMENT_MAX_EXTRACT_CHARS設定4000過剰な抽出からのスパムを防止
ASYNC_COMPLETION_SANITIZE設定true非同期パスでは有効なままにする必要がある

エビデンスとソース

このトラブルシューティングガイドは、FixClaw Intelligence パイプラインによってコミュニティの議論から自動的に合成されました。