April 22, 2026 • 版本: v2026.4.14

[升级v2026.4.14后专用网络音频转录SSRF拦截错误] - SSRF Block on Private Network Audio Transcription After v2026.4.14 Upgrade

语音消息转录到专用IP的自托管STT端点失败,即使已配置 models.providers.*.request.allowPrivateNetwork,仍会抛出 SsrFBlockedError,这是由提供商请求解析中的两个级联bug导致的。

🔍 症状

主要错误表现

向专用 LAN IP 上的 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

受影响的操作

  • 通过自托管 STT 端点上的 parakeet 模型进行语音消息转录
  • 任何针对专用/内部 IP 地址的 media.audio 工具调用
  • LAN 地址上的 OpenAI 兼容 API 端点(例如 192.168.x.x10.x.x.x172.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" }]
      }
    }
  }
}

版本上下文

VersionBehavior
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 版本中的两个独立缺陷形成级联,悄悄地从提供商的请求设置中丢弃了 allowPrivateNetwork 配置。两个缺陷必须同时存在才能复现失败,两个缺陷都必须修复才能恢复正常行为。


缺陷 1:resolveProviderExecutionContext 从提供商配置中丢弃 allowPrivateNetwork

受影响文件: runner.entries-*.js(dist 文件)

代码路径:

resolveProviderExecutionContext 函数通过以下合并链构建传递给 transcribeAudiorequest 对象:

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 传递。

调用方(例如 transcribeOpenAiCompatibleAudiotranscribeDeepgramAudio)设置:

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 文件的热修复(立即生效)

此方法直接修补编译后的分发文件。适用于容器化部署或在无法立即从源码重建时。

前置条件

  • 访问运行中容器的文件系统或部署环境
  • 两个受影响的 dist 文件:
    • runner.entries-*.js
    • provider-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 — 通配符(仅影响匹配的 tools)

正确方法: 在缺陷修复之前,始终为音频转录在工具级设置 allowPrivateNetwork


2. 假设配置被忽略时架构验证会通过

陷阱: 架构将 allowPrivateNetwork 验证为有效字段,因此操作员认为它正在被使用。

详情: models.providers.*.request 的 JSON schema 包含 allowPrivateNetwork(在 v2026.4.12 的 #63671 中添加)。但是,代码路径从不读取音频转录的此字段。这造成了误判 — 配置看起来有效但被悄悄丢弃。

变通方案: 始终使用实际测试请求验证行为,而不仅仅是架构验证。


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 — 此回归中的主要错误。当 SSRF 策略阻止对未明确允许的主机名或 IP 地址的请求时发生。

    [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 schema 和初始实现。此 PR 添加了 v2026.4.14 回归的功能。

  • Issue #64201 — 通过重定向绕过 SSRF — 关于允许专用网络访问的历史问题。当前回归的任何修复都应确保重定向目标也受 allowPrivateNetwork 检查约束。

  • Issue #63847 — 大文件媒体转录超时 — 与音频转录可靠性相关;可能与此回归共享错误处理路径。

  • Issue #64012 — 提供商配置合并优先级不明确 — 记录了关于哪个配置级别优先的困惑。因为缺陷 1 源于提供商配置未被包含在合并中,所以相关。


相似错误模式

Error CodeDescriptionDistinction
NetworkError通用网络故障无 SSRF 组件;通常是 DNS/连接被拒绝
CertificateErrorTLS/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)"

依据与来源

本故障排除指南由 FixClaw 智能管线从社区讨论中自动合成。