SSRF Block on Private Network Audio Transcription After v2026.4.14 Upgrade
Voice message transcription to self-hosted STT endpoints on private IPs fails with SsrFBlockedError even when models.providers.*.request.allowPrivateNetwork is configured, caused by two cascading bugs in provider request resolution.
๐ Symptoms
Primary Error Manifestation
Audio transcription requests to OpenAI-compatible endpoints on private LAN IPs are blocked with an SSRF violation, despite explicit configuration allowing private network access.
[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=SsrFBlockedErrorAffected Operations
- Voice message transcription via
parakeetmodel on self-hosted STT endpoints - Any
media.audiotool invocation targeting private/internal IP addresses - OpenAI-compatible API endpoints on LAN addresses (e.g.,
192.168.x.x,10.x.x.x,172.16.x.x)
Configuration That Should Work
The following configuration is the documented approach for enabling private network access to STT providers:
{
"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" }]
}
}
}
}Version Context
| Version | Behavior |
|---|---|
| v2026.4.12 | โ Works correctly |
| v2026.4.13 | โ Works correctly |
| v2026.4.14 | โ SSRF blocked โ regression introduced |
Diagnostic Command Output
When debugging is enabled, the following may appear in logs:
# 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๐ง Root Cause
Overview
Two independent bugs in the v2026.4.14 release form a cascade that silently drops the allowPrivateNetwork configuration from provider request settings. Both bugs must be present to reproduce the failure, and both must be fixed to restore correct behavior.
Bug 1: resolveProviderExecutionContext Drops allowPrivateNetwork from Provider Config
Affected File: runner.entries-*.js (dist file)
Code Path:
The resolveProviderExecutionContext function constructs the request object passed to transcribeAudio through the following merge chain:
javascript request: mergeProviderRequestOverrides( sanitizeConfiguredProviderRequest(params.config?.request), sanitizeConfiguredProviderRequest(params.entry.request) )
Root Cause:
The function merges only:
params.config?.requestโ tool-level request config (fromtools.media.audio.request)params.entry.requestโ entry-level request overrides
Critically, the provider-level configuration (models.providers.<id>.request) is never included in this merge. The sanitizeConfiguredProviderRequest function explicitly filters to only these fields:
javascript // Fields preserved by sanitizeConfiguredProviderRequest const ALLOWED_REQUEST_FIELDS = [‘headers’, ‘auth’, ‘proxy’, ’tls’]; // Note: ‘allowPrivateNetwork’ is intentionally NOT in this list
Result: Even when an operator correctly configures:
json “models”: { “providers”: { “openai”: { “request”: { “allowPrivateNetwork”: true } } } }
This value is silently discarded because the provider config is never consulted during request object construction for audio transcription.
Bug 2: resolveProviderRequestPolicyConfig Ignores allowPrivateNetwork in params.request
Affected File: provider-request-config-*.js (dist file)
Code Path:
The resolveProviderRequestPolicyConfig function returns the resolved security policy:
javascript allowPrivateNetwork: params.allowPrivateNetwork ?? false
Root Cause:
The function checks only params.allowPrivateNetwork โ a direct parameter that callers must pass explicitly. However, all audio transcription callers derive their request config from resolveProviderHttpRequestConfig and pass it as request: params.request.
The callers (e.g., transcribeOpenAiCompatibleAudio, transcribeDeepgramAudio) set:
javascript { request: resolveProviderHttpRequestConfig({ model: params.model, provider: params.provider, // allowPrivateNetwork IS present here from Bug 1 fix }) }
But resolveProviderRequestPolicyConfig receives params.request?.allowPrivateNetwork and never checks it. The value is only read from the flat params.allowPrivateNetwork parameter, which audio callers do not set explicitly.
Result: Even if Bug 1 were fixed and allowPrivateNetwork was correctly placed in params.request, Bug 2 would silently discard it during policy resolution.
Failure Sequence Diagram
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ 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 โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Why Both Bugs Are Required
| Bug 1 Fixed? | Bug 2 Fixed? | Result |
|---|---|---|
| โ No | โ No | SSRF block (current broken state) |
| โ Yes | โ No | SSRF block (Bug 2 still discards the value) |
| โ No | โ Yes | No change (Bug 1 never populates the value) |
| โ Yes | โ Yes | โ Correct behavior restored |
๐ ๏ธ Step-by-Step Fix
Option 1: Hotfix Applied to Dist Files (Immediate)
This approach patches the compiled distribution files directly. Suitable for containerized deployments or when rebuilding from source is not immediately possible.
Prerequisites
- Access to the running container filesystem or deployment environment
- The two affected dist files:
runner.entries-*.jsprovider-request-config-*.js
Step 1: Locate the Affected Files
# 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-*.jsStep 2: Patch runner.entries-*.js (Bug 1 Fix)
Before (Buggy): javascript request: mergeProviderRequestOverrides( sanitizeConfiguredProviderRequest(params.config?.request), sanitizeConfiguredProviderRequest(params.entry.request) )
After (Fixed): javascript request: mergeModelProviderRequestOverrides( sanitizeConfiguredModelProviderRequest(providerConfig?.request), sanitizeConfiguredProviderRequest(params.config?.request), sanitizeConfiguredProviderRequest(params.entry.request) )
Step 3: Patch provider-request-config-*.js (Bug 2 Fix)
Before (Buggy): javascript allowPrivateNetwork: params.allowPrivateNetwork ?? false
After (Fixed): javascript allowPrivateNetwork: params.allowPrivateNetwork ?? params.request?.allowPrivateNetwork ?? false
Step 4: Restart the Application
# For Docker containers
docker-compose restart openclaw
# For Kubernetes
kubectl rollout restart deployment/openclaw
# For systemd
sudo systemctl restart openclawOption 2: Configuration Workaround (No Code Changes)
If you cannot modify the dist files immediately, you can work around the bug by specifying allowPrivateNetwork at the tool-level config instead of the provider level.
Configuration Change
Before (Provider-Level โ Does Not Work in 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” }] } } } }
After (Tool-Level Workaround): json { “models”: { “providers”: { “openai”: { “baseUrl”: “http://192.168.x.x:5092/v1" } } }, “tools”: { “media”: { “audio”: { “request”: { “allowPrivateNetwork”: true }, “models”: [{ “provider”: “openai”, “model”: “parakeet” }] } } } }
Note: This workaround places allowPrivateNetwork in tools.media.audio.request, which is checked via params.config?.request in the existing merge chain. However, this must be applied to every tool configuration that needs private network access.
Option 3: Permanent Fix via Source Modification (Recommended)
For long-term resolution, apply the fixes to the source TypeScript files before building.
Bug 1 Fix โ Source File
File: src/api/worker/runner/entries.ts (or equivalent)
Change: 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) );
Bug 2 Fix โ Source File
File: src/api/providers/provider-request-config.ts (or equivalent)
Change: typescript // Before const allowPrivateNetwork = params.allowPrivateNetwork ?? false;
// After const allowPrivateNetwork = params.allowPrivateNetwork ?? params.request?.allowPrivateNetwork ?? false;
Rebuild and Deploy
# 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๐งช Verification
Prerequisites for Verification
Ensure you have:
- A self-hosted STT endpoint on a private IP (e.g., Parakeet on
192.168.1.100:5092) - A test audio file for transcription
- Access to the deployment logs
Step 1: Verify Configuration Is Loaded
Check that your provider config with allowPrivateNetwork is properly recognized:
# 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 } }Step 2: Enable Security Debug Logging
# Set debug environment variable
export DEBUG=openclaw:security:*
# Or in docker-compose.yml:
# environment:
# - DEBUG=openclaw:security:*Step 3: Execute Test Transcription
# 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"
}'Step 4: Verify Success (Fixed Behavior)
Expected Output (Success):
[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 errorDebug Log Verification (Fixed):
[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 falseStep 5: Verify Failure State (Baseline)
If the fix is not applied, you will see:
[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 configStep 6: Regression Test Suite
Create a test script to verify both scenarios:
#!/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 ==="โ ๏ธ Common Pitfalls
1. Misunderstanding Configuration Hierarchy
Pitfall: Operators place allowPrivateNetwork in the wrong location, expecting it to propagate automatically.
Details:
models.providers.<id>.request.allowPrivateNetworkโ provider-level (broken in v2026.4.14)tools.media.audio.request.allowPrivateNetworkโ tool-level (works, but must be set explicitly per tool)tools.*.request.allowPrivateNetworkโ wildcard (only affects matching tools)
Correct Approach: Until the bug is fixed, always set allowPrivateNetwork at the tool level for audio transcription.
2. Assuming Schema Validation Passes When Config Is Ignored
Pitfall: The schema validates allowPrivateNetwork as a valid field, so operators assume it’s being used.
Details: The JSON schema for models.providers.*.request includes allowPrivateNetwork (added in #63671 for v2026.4.12). However, the code path never reads this field for audio transcription. This creates a false positive โ the config appears valid but is silently discarded.
Workaround: Always verify behavior with actual test requests, not just schema validation.
3. Docker Volume Mount Conflicts
Pitfall: When using volume mounts to patch dist files, the original files may be restored on container restart.
Details: If you patch dist/ files directly in a container:
yaml
docker-compose.yml
volumes:
- ./patched-runner.entries.js:/app/dist/api/worker/runner.entries-abc123.js
The container restart policy or image rebuild will overwrite your patch.
Solution: Use a derived image with the patch baked in: 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. Cache Propagation Delay
Pitfall: After fixing the config, audio transcription still fails due to cached provider resolution.
Details: OpenClaw caches resolved provider configurations. A fix to models.providers.<id>.request.allowPrivateNetwork may not take effect until:
- The cache TTL expires
- The application restarts
- The cache is explicitly cleared
Commands: bash
Clear configuration cache
rm -rf ~/.openclaw/cache/* rm -rf /tmp/openclaw-*
Or restart the service
systemctl restart openclaw
5. Conflicting Security Policies
Pitfall: Even with allowPrivateNetwork: true, a global security policy may override it.
Details: Check for conflicting configurations: json { “security”: { “networkPolicy”: { “allowPrivate”: false // This would override provider-level settings } } }
Verification: bash openclaw config show –path security
6. IPv6 Private Addresses
Pitfall: allowPrivateNetwork may not cover all IPv6 private address ranges.
Details: The following may still be blocked even with allowPrivateNetwork: true:
::1(loopback)fc00::/7(unique local addresses)fe80::/10(link-local addresses)
Solution: If using IPv6 private addresses, verify the SSRF policy explicitly includes them: bash DEBUG=openclaw:security:* openclaw media transcribe … 2>&1 | grep -i “ipv6|private”
7. TLS/SSL Verification Conflicts
Pitfall: Private network endpoints often use self-signed certificates. If request.tls.rejectUnauthorized is not set, requests may fail with certificate errors.
Details: A complete private network config should include: json { “models”: { “providers”: { “openai”: { “baseUrl”: “https://192.168.1.100:5092/v1”, “request”: { “allowPrivateNetwork”: true, “tls”: { “rejectUnauthorized”: false } } } } } }
๐ Related Errors
Directly Related Errors
SsrFBlockedErrorโ The primary error seen in this regression. Occurs when the SSRF policy blocks a request to a hostname or IP address not explicitly allowed.[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โ General URL fetch failure, may occur if SSRF is not the blocking mechanism but the URL is otherwise invalid.ProviderRequestConfigErrorโ Thrown when request configuration cannot be resolved. May occur if the merge chain encounters undefined values.
Historically Related Issues
PR #63671 (v2026.4.12) โ Introduced the
models.providers.*.request.allowPrivateNetworkschema and initial implementation. This PR added the capability that v2026.4.14 regresses.Issue #64201 โ SSRF bypass via redirect โ Historical concern about allowing private network access. Any fix for the current regression should ensure redirect targets are also subject to
allowPrivateNetworkchecks.Issue #63847 โ Media transcription timeout on large files โ Related to audio transcription reliability; may share error handling paths with this regression.
Issue #64012 โ Provider config merge priority unclear โ Documents the confusion around which config level takes precedence. Relevant because Bug 1 stems from provider config not being included in the merge.
Similar Error Patterns
| Error Code | Description | Distinction |
|---|---|---|
NetworkError | Generic network failure | No SSRF component; usually DNS/connection refused |
CertificateError | TLS/SSL validation failure | Related to private network self-signed certs |
TimeoutError | Request timeout | May be misdiagnosed if SSRF blocks fast |
AuthenticationError | Auth failure | Unrelated; indicates wrong credentials |
Debugging Reference
When filing issues related to this regression, include:
# 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)"