v2026.4.14アップデート後のプライベートネットワーク音声書き起こしにおけるSSRFブロック - SSRF Block on Private Network Audio Transcription After v2026.4.14 Upgrade
models.providers.*.request.allowPrivateNetworkが設定されていても、プライベートIPの自己ホストSTTエンドポイントへの音声メッセージの書き起こしがSsrFBlockedErrorで失敗します。これはプロバイダー要求解決における2つの連鎖バグによって引き起こされます。
🔍 症状
主なエラーの症状
OpenAI互換エンドポイントへの音声文字起こしリクエストが、プライベートネットワークアクセスが明示的に許可されているにもかかわらず、SSRF違反でブロックされます。
[security] blocked URL fetch (url-fetch) target=http://192.168.x.x:5092/v1/audio/transcriptions reason=Blocked hostname or private/internal/special-use IP address
[media-understanding] audio: failed (0/1) reason=SsrFBlockedError影響を受ける操作
- プライベートLAN IP上の自己ホスト型STTエンドポイントでの
parakeetモデルによる音声メッセージの文字起こし - プライベート/内部IPアドレスをターゲットとする
media.audioツール呼び出し - LANアドレス上のOpenAI互換APIエンドポイント(例:
192.168.x.x、10.x.x.x、172.16.x.x)
動作するはずの設定
以下は、STTプロバイダーへのプライベートネットワークアクセスを有効にするための文書化されたアプローチです:
{
"models": {
"providers": {
"openai": {
"apiKey": "local",
"baseUrl": "http://192.168.x.x:5092/v1",
"request": {
"allowPrivateNetwork": true
}
}
}
},
"tools": {
"media": {
"audio": {
"enabled": true,
"models": [{ "provider": "openai", "model": "parakeet" }]
}
}
}
}バージョンコンテキスト
| バージョン | 動作 |
|---|---|
| v2026.4.12 | ✅ 正しく動作 |
| v2026.4.13 | ✅ 正しく動作 |
| v2026.4.14 | ❌ SSRFでブロック — リグレッションが発生 |
診断コマンドの出力
デバッグが有効な場合、ログに以下が表示される場合があります:
# Enable debug logging to trace request resolution
DEBUG=openclaw:* node index.js
# Expected SSRF block in output:
[security] blocked URL fetch (url-fetch) target=http://192.168.x.x:5092/v1/audio/transcriptions reason=Blocked hostname or private/internal/special-use IP address🧠 原因
概要
v2026.4.14リリースにおける2つの独立したバグが連鎖して、プロバイダーリクエスト設定からallowPrivateNetwork設定をサイレントにドロップします。両方のバグが障害再現に必須であり、正常な動作を回復するには両方を修正する必要があります。
バグ1: resolveProviderExecutionContextがプロバイダ設定からallowPrivateNetworkをドロップ
影響を受けるファイル: runner.entries-*.js(distファイル)
コードパス:
resolveProviderExecutionContext関数は、以下のマージチェーンを通じてtranscribeAudioに渡されるrequestオブジェクトを構築します:
javascript request: mergeProviderRequestOverrides( sanitizeConfiguredProviderRequest(params.config?.request), sanitizeConfiguredProviderRequest(params.entry.request) )
原因:
この関数は以下のみをマージします:
params.config?.request— ツールレベルのリクエスト設定(tools.media.audio.requestから)params.entry.request— エントリレベルのリクエストオーバーライド
重要な点として、プロバイダーレベルの設定(models.providers.<id>.request)はこのマージに決して含まれません。sanitizeConfiguredProviderRequest関数は以下のフィールドのみを明示的にフィルタリングします:
javascript // Fields preserved by sanitizeConfiguredProviderRequest const ALLOWED_REQUEST_FIELDS = [‘headers’, ‘auth’, ‘proxy’, ’tls’]; // Note: ‘allowPrivateNetwork’ is intentionally NOT in this list
結果: オペレーターが以下のように正しく設定しても:
json “models”: { “providers”: { “openai”: { “request”: { “allowPrivateNetwork”: true } } } }
この値はプロバイダ設定が音声文字起こしのリクエストオブジェクト構築時に参照されないため、サイレントに破棄されます。
バグ2: resolveProviderRequestPolicyConfigがparams.requestのallowPrivateNetworkを無視
影響を受けるファイル: provider-request-config-*.js(distファイル)
コードパス:
resolveProviderRequestPolicyConfig関数は解決されたセキュリティポリシーを返します:
javascript allowPrivateNetwork: params.allowPrivateNetwork ?? false
原因:
この関数はparams.allowPrivateNetworkのみをチェックします — これは呼び出し元が明示的に渡す必要がある直接パラメータです。しかし、すべての音声文字起こしの呼び出し元はresolveProviderHttpRequestConfigからリクエスト設定を導出し、request: params.requestとして渡します。
呼び出し元(例:transcribeOpenAiCompatibleAudio、transcribeDeepgramAudio)は以下を設定します:
javascript { request: resolveProviderHttpRequestConfig({ model: params.model, provider: params.provider, // allowPrivateNetwork IS present here from Bug 1 fix }) }
しかしresolveProviderRequestPolicyConfigはparams.request?.allowPrivateNetworkを受け取り、それをチェックしません。値は平面的なparams.allowPrivateNetworkパラメータからのみ読み取られます。これは音声呼び出し元が明示的に設定しません。
結果: バグ1が修正されallowPrivateNetworkが正しくparams.requestに配置されたとしても、バグ2はポリシ解決中にそれをサイレントに破棄します。
障害シーケンス図
┌─────────────────────────────────────────────────────────────────────────┐ │ CORRECT BEHAVIOR (v2026.4.12/13) │ ├─────────────────────────────────────────────────────────────────────────┤ │ models.providers.openai.request.allowPrivateNetwork = true │ │ │ │ │ ▼ │ │ resolveProviderExecutionContext() merges provider config │ │ │ │ │ ▼ │ │ request.allowPrivateNetwork = true (propagated correctly) │ │ │ │ │ ▼ │ │ resolveProviderRequestPolicyConfig() reads from request object │ │ │ │ │ ▼ │ │ Audio transcription succeeds on private IP endpoint │ └─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐ │ BROKEN BEHAVIOR (v2026.4.14) │ ├─────────────────────────────────────────────────────────────────────────┤ │ models.providers.openai.request.allowPrivateNetwork = true │ │ │ │ │ ┌───────────────────┴───────────────────┐ │ │ ▼ ▼ │ │ [BUG 1] resolveProviderExecutionContext() Provider config │ │ NEVER includes provider config → allowPrivateNetwork = undefined │ │ │ │ │ ▼ │ │ mergeProviderRequestOverrides() produces request without │ │ allowPrivateNetwork field │ │ │ │ │ ▼ │ │ [BUG 2] resolveProviderRequestPolicyConfig() │ │ only checks params.allowPrivateNetwork (not params.request) │ │ → allowPrivateNetwork = false (default) │ │ │ │ │ ▼ │ │ SSRF policy blocks private IP → SsrFBlockedError │ └─────────────────────────────────────────────────────────────────────────┘
両方のバグが必要な理由
| バグ1を修正? | バグ2を修正? | 結果 |
|---|---|---|
| ❌ いいえ | ❌ いいえ | SSRFブロック(現在の障害状態) |
| ✅ はい | ❌ いいえ | SSRFブロック(バグ2がまだ値を破棄) |
| ❌ いいえ | ✅ はい | 変化なし(バグ1が値を設定しない) |
| ✅ はい | ✅ はい | ✅ 正常な動作が回復 |
🛠️ 解決手順
オプション1: Distファイルへのホットフィックス(即時適用)
このアプローチはコンパイル済み配布ファイルを直接パッチ当てします。コンテナ化されたデプロイメントやソースからの再ビルドがすぐにできない場合に適しています。
前提条件
- 実行中のコンテナファイルシステムまたはデプロイメント環境へのアクセス
- 影響を受ける2つのdistファイル:
runner.entries-*.jsprovider-request-config-*.js
ステップ1: 影響を受けるファイルの検索
# Find the dist files in your deployment
find /app -name "runner.entries-*.js" 2>/dev/null
find /app -name "provider-request-config-*.js" 2>/dev/null
# Typical container paths:
# /app/dist/api/worker/runner.entries-*.js
# /app/dist/api/providers/provider-request-config-*.jsステップ2: runner.entries-*.jsのパッチ適用(バグ1の修正)
修正前(バグあり): javascript request: mergeProviderRequestOverrides( sanitizeConfiguredProviderRequest(params.config?.request), sanitizeConfiguredProviderRequest(params.entry.request) )
修正後(修正済み): javascript request: mergeModelProviderRequestOverrides( sanitizeConfiguredModelProviderRequest(providerConfig?.request), sanitizeConfiguredProviderRequest(params.config?.request), sanitizeConfiguredProviderRequest(params.entry.request) )
ステップ3: provider-request-config-*.jsのパッチ適用(バグ2の修正)
修正前(バグあり): javascript allowPrivateNetwork: params.allowPrivateNetwork ?? false
修正後(修正済み): javascript allowPrivateNetwork: params.allowPrivateNetwork ?? params.request?.allowPrivateNetwork ?? false
ステップ4: アプリケーションの再起動
# For Docker containers
docker-compose restart openclaw
# For Kubernetes
kubectl rollout restart deployment/openclaw
# For systemd
sudo systemctl restart openclawオプション2: 設定による回避策(コード変更なし)
distファイルをすぐに変更できない場合、プロバイダーレベルではなくツールレベル設定でallowPrivateNetworkを指定することで回避できます。
設定の変更
修正前(プロバイダーレベル — v2026.4.14では動作しない): json { “models”: { “providers”: { “openai”: { “baseUrl”: “http://192.168.x.x:5092/v1", “request”: { “allowPrivateNetwork”: true } } } }, “tools”: { “media”: { “audio”: { “models”: [{ “provider”: “openai”, “model”: “parakeet” }] } } } }
修正後(ツールレベルの回避策): json { “models”: { “providers”: { “openai”: { “baseUrl”: “http://192.168.x.x:5092/v1" } } }, “tools”: { “media”: { “audio”: { “request”: { “allowPrivateNetwork”: true }, “models”: [{ “provider”: “openai”, “model”: “parakeet” }] } } } }
注意: この回避策ではallowPrivateNetworkをtools.media.audio.requestに配置します。これは既存のマージチェーンでparams.config?.request経由でチェックされます。ただし、プライベートネットワークアクセスが必要なすべてのツール設定に適用する必要があります。
オプション3: ソース修正による恒久的な修正(推奨)
長期的な解決には、ビルド前にTypeScriptソースファイルに修正を適用します。
バグ1の修正 — ソースファイル
ファイル: src/api/worker/runner/entries.ts(または同等のファイル)
変更: typescript // Before const request = mergeProviderRequestOverrides( sanitizeConfiguredProviderRequest(params.config?.request), sanitizeConfiguredProviderRequest(params.entry.request) );
// After const request = mergeModelProviderRequestOverrides( sanitizeConfiguredModelProviderRequest(providerConfig?.request), sanitizeConfiguredProviderRequest(params.config?.request), sanitizeConfiguredProviderRequest(params.entry.request) );
バグ2の修正 — ソースファイル
ファイル: src/api/providers/provider-request-config.ts(または同等のファイル)
変更: typescript // Before const allowPrivateNetwork = params.allowPrivateNetwork ?? false;
// After const allowPrivateNetwork = params.allowPrivateNetwork ?? params.request?.allowPrivateNetwork ?? false;
再ビルドとデプロイ
# Rebuild the application
npm run build
# Or with specific build command
pnpm build
# Redeploy
docker build -t openclaw:fixed .
docker push your-registry/openclaw:fixed
kubectl rollout restart deployment/openclaw🧪 検証
検証の前提条件
以下を確認してください:
- プライベートIP上の自己ホスト型STTエンドポイント(例:
192.168.1.100:5092上のParakeet) - 文字起こし用のテストオーディオファイル
- デプロイメントログへのアクセス
ステップ1: 設定が読み込まれていることを確認
allowPrivateNetworkを含むプロバイダー設定が適切に認識されていることを確認します:
# Check loaded configuration (if CLI exposes this)
openclaw config show --path "models.providers.openai.request"
# Expected output:
# { allowPrivateNetwork: true }
# Or in debug logs, look for:
# [config] loaded provider config: openai { ..., request: { allowPrivateNetwork: true } }ステップ2: セキュリティデバッグログを有効化
# Set debug environment variable
export DEBUG=openclaw:security:*
# Or in docker-compose.yml:
# environment:
# - DEBUG=openclaw:security:*ステップ3: テスト文字起こしを実行
# Create a test audio file (silence or short recording)
ffmpeg -f lavfi -i anullsrc=r=16000:cl=mono -t 1 -acodec pcm_s16le /tmp/test.wav
# Execute transcription via OpenClaw CLI or API
openclaw media transcribe \
--provider openai \
--model parakeet \
--audio @/tmp/test.wav \
--url http://192.168.1.100:5092/v1/audio/transcriptions
# Or via API
curl -X POST http://localhost:3000/api/media/transcribe \
-H "Content-Type: application/json" \
-d '{
"provider": "openai",
"model": "parakeet",
"audioUrl": "http://192.168.1.100:5092/v1/audio/transcriptions"
}'ステップ4: 成功を確認(修正済み動作)
** 예상出力(成功):**
[media-understanding] audio: processing (1/1) provider=openai model=parakeet
[media-understanding] audio: completed (1/1) provider=openai model=parakeet duration=1.2s
# Transcription result should be returned without SSRF errorデバッグログの確認(修正済み):
[security] resolving request policy for provider=openai
[security] allowPrivateNetwork=true (resolved from request config)
[security] URL fetch allowed: target=http://192.168.1.100:5092/v1/audio/transcriptions
# Should see allowPrivateNetwork=true in logs, not falseステップ5: 障害状態を確認(ベースライン)
修正が適用されていない場合、以下が表示されます:
[security] blocked URL fetch (url-fetch) target=http://192.168.1.100:5092/v1/audio/transcriptions reason=Blocked hostname or private/internal/special-use IP address
[media-understanding] audio: failed (0/1) reason=SsrFBlockedError
[security] allowPrivateNetwork=false (default fallback)
# Note: The second line shows the bug — should be true from configステップ6: 回帰テストスイート
両方のシナリオを検証するテストスクリプトを作成します:
#!/bin/bash
# test-allow-private-network.sh
set -e
echo "=== Test 1: Provider-level allowPrivateNetwork ==="
openclaw config set models.providers.test.request.allowPrivateNetwork true
openclaw media transcribe \
--provider test \
--model test-model \
--audio @/tmp/test.wav \
--url http://192.168.1.100:5092/v1/audio/transcriptions
if [ $? -eq 0 ]; then
echo "✅ Test 1 PASSED: Private network access granted"
else
echo "❌ Test 1 FAILED: SSRF blocked the request"
exit 1
fi
echo ""
echo "=== Test 2: Verify config is not silently dropped ==="
DEBUG=openclaw:security:* openclaw media transcribe \
--provider test \
--model test-model \
--audio @/tmp/test.wav \
--url http://192.168.1.100:5092/v1/audio/transcriptions 2>&1 | grep -i "allowPrivateNetwork"
echo ""
echo "=== All tests completed ==="⚠️ よくある落とし穴
1. 設定の階層構造の誤解
落とし穴: オペレーターがallowPrivateNetworkを誤った場所に配置し、自動的に伝播されると思い込みます。
詳細:
models.providers.<id>.request.allowPrivateNetwork— プロバイダーレベル(v2026.4.14で障害)tools.media.audio.request.allowPrivateNetwork— ツールレベル(動作するがツールごとに明示的に設定が必要)tools.*.request.allowPrivateNetwork— ワイルドカード(一致するツールのみに影響)
正しいアプローチ: バグが修正されるまで、音声文字起こしには常にツールレベルでallowPrivateNetworkを設定してください。
2. 設定が無視されてもスキーマ検証が通ると想定
落とし穴: スキーマがallowPrivateNetworkを有効なフィールドとして検証するため、オペレーターは使用されていると思い込みます。
詳細: models.providers.*.requestのJSONスキーマにはallowPrivateNetworkが含まれています(v2026.4.12向けに#63671で追加)。しかし、コードパスは音声文字起こしのこのフィールドを読み取りません。これによりフォalseポジティブが発生します — 設定は有効に見えますが、サイレントに破棄されます。
回避策: 実際のテストリクエストで動作を必ず確認し、スキーマ検証だけでなくデバッグログも確認してください。
3. Dockerボリュームマウントの競合
落とし穴: distファイルにパッチを当てるためにボリュームマウントを使用すると、コンテナ再起動時に元のファイルが復元される場合があります。
詳細: コンテナ内で直接dist/ファイルにパッチを当てる場合:
yaml
docker-compose.yml
volumes:
- ./patched-runner.entries.js:/app/dist/api/worker/runner.entries-abc123.js
コンテナの再起動ポリシーまたはイメージの再ビルドにより、パッチが上書きされます。
解決策: パッチを組み込んだ派生イメージを使用します: dockerfile FROM openclaw:2026.4.14 COPY patched-runner.entries.js /app/dist/api/worker/runner.entries-abc123.js COPY patched-provider-request-config.js /app/dist/api/providers/provider-request-config-xyz789.js
4. キャッシュ伝播の遅延
落とし穴: 設定修正後も、プロバイダー解決のキャッシュにより音声文字起こしが失敗し続ける場合があります。
詳細: OpenClawは解決されたプロバイダー設定をキャッシュします。models.providers.<id>.request.allowPrivateNetworkへの修正は、以下が完了するまで有効にならない場合があります:
- キャッシュTTLの期限切れ
- アプリケーションの再起動
- キャッシュの明示的なクリア
コマンド: bash
Clear configuration cache
rm -rf ~/.openclaw/cache/* rm -rf /tmp/openclaw-*
Or restart the service
systemctl restart openclaw
5. 競合するセキュリティポリシー
落とし穴: allowPrivateNetwork: trueを設定しても、グローバルセキュリティポリシーによって上書きされる場合があります。
詳細: 競合する設定を確認します: json { “security”: { “networkPolicy”: { “allowPrivate”: false // This would override provider-level settings } } }
確認: bash openclaw config show –path security
6. IPv6プライベートアドレス
落とし穴: allowPrivateNetworkはすべてのIPv6プライベートアドレス範囲をカバーしない場合があります。
詳細: allowPrivateNetwork: trueを設定しても、以下はブロックされる場合があります:
::1(ループバック)fc00::/7(ユニークローカルアドレス)fe80::/10(リンクローカルアドレス)
解決策: IPv6プライベートアドレスを使用する場合は、SSRFポリシーがそれらを含んでいることを明示的に確認します: bash DEBUG=openclaw:security:* openclaw media transcribe … 2>&1 | grep -i “ipv6|private”
7. TLS/SSL検証の競合
落とし穴: プライベートネットワークエンドポイントは自己署名証明書をよく使用します。request.tls.rejectUnauthorizedが設定されていない場合、証明書エラーでリクエストが失敗する可能性があります。
詳細: 完全なプライベートネットワーク設定には以下を含める必要があります: json { “models”: { “providers”: { “openai”: { “baseUrl”: “https://192.168.1.100:5092/v1”, “request”: { “allowPrivateNetwork”: true, “tls”: { “rejectUnauthorized”: false } } } } } }
🔗 関連するエラー
的直接に関連するエラー
SsrFBlockedError— このリグレッションで表示される主なエラー。明示的に許可されていないホスト名またはIPアドレスへのリクエストをSSRFポリシーがブロックしたときに発生します。[security] blocked URL fetch (url-fetch) target=http://192.168.x.x:5092/v1/audio/transcriptions reason=Blocked hostname or private/internal/special-use IP address
UrlFetchError— 一般的なURLフェッチ失敗。SSRFがブロックメカニズムでないがURLが無効な場合 발생할 수 있습니다.ProviderRequestConfigError— リクエスト設定が解決できない場合にスローされます。マージチェーンが未定義の値に遭遇した場合 발생할 수 있습니다.
歴史的に関連する問題
PR #63671 (v2026.4.12) —
models.providers.*.request.allowPrivateNetworkスキーマと初期実装を導入しました。このPRはv2026.4.14がリグレッション起こした機能を追加しました。Issue #64201 — リダイレクトによるSSRFバイパス — プライベートネットワークアクセスを許可することに関する歴史的な懸念事項。この現在のリグレッションの修正では、リダイレクトターゲットも
allowPrivateNetworkチェックの対象になることを確認する必要があります。Issue #63847 — 大容量ファイルのメディア文字起こしタイムアウト — 音声文字起こしの信頼性に関連。このリグレッションとエラー処理パスを共有する可能性があります。
Issue #64012 — プロバイダー設定のマージ優先順位が不明確 — どの設定レベルが優先されるかわからないという混乱を記録しています。バグ1がプロバイダー設定がマージに含まれていないことから生じているため関連性があります。
類似のエラーパターン
| エラーコード | 説明 | 区別 |
|---|---|---|
NetworkError | 一般的なネットワーク障害 | SSRFコンポーネントなし。通常はDNS/接続拒否 |
CertificateError | TLS/SSL検証失敗 | プライベートネットワークの自己署名証明書を参照 |
TimeoutError | リクエストタイムアウト | SSRFが高速にブロックする場合、誤診される可能性あり |
AuthenticationError | 認証失敗 | 関連なし。認証情報が正しくないことを示します |
デバッグ参照
このリグレッション関連する問題を報告する場合は、以下を含めます:
# Environment info
openclaw --version
# Expected: v2026.4.14
# Configuration (sanitized)
cat config.json | jq '.models.providers | keys'
# Debug logs with security trace
DEBUG=openclaw:* node index.js 2>&1 | grep -E "(allowPrivateNetwork|SsrFBlocked|provider-request)"
# Provider resolution trace
DEBUG=openclaw:provider:* node index.js 2>&1 | grep -E "(resolveProvider|ExecutionContext)"