April 28, 2026 โ€ข Version: v2026.4.14

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=SsrFBlockedError

Affected Operations

  • Voice message transcription via parakeet model on self-hosted STT endpoints
  • Any media.audio tool 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

VersionBehavior
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 (from tools.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โŒ NoSSRF block (current broken state)
โœ… YesโŒ NoSSRF block (Bug 2 still discards the value)
โŒ Noโœ… YesNo 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-*.js
    • provider-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-*.js

Step 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 openclaw

Option 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.


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 error

Debug 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 false

Step 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 config

Step 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 } } } } } }

  • 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.


  • PR #63671 (v2026.4.12) โ€” Introduced the models.providers.*.request.allowPrivateNetwork schema 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 allowPrivateNetwork checks.

  • 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 CodeDescriptionDistinction
NetworkErrorGeneric network failureNo SSRF component; usually DNS/connection refused
CertificateErrorTLS/SSL validation failureRelated to private network self-signed certs
TimeoutErrorRequest timeoutMay be misdiagnosed if SSRF blocks fast
AuthenticationErrorAuth failureUnrelated; 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)"

Evidence & Sources

This troubleshooting guide was automatically synthesized by the FixClaw Intelligence Pipeline from community discussions.