[Bloqueo SSRF en transcripción de audio a red privada tras actualización] - SSRF Block on Private Network Audio Transcription After v2026.4.14 Upgrade
La transcripción de mensajes de voz a endpoints STT autoalojados en IPs privadas falla con SsrFBlockedError incluso cuando models.providers.*.request.allowPrivateNetwork está configurado, causado por dos bugs en cascada en la resolución de solicitudes del proveedor.
🔍 Síntomas
Manifestación principal del error
Las solicitudes de transcripción de audio a puntos de conexión compatibles con OpenAI en IPs privadas de LAN se bloquean con una violación SSRF, a pesar de la configuración explícita que permite el acceso a redes privadas.
[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=SsrFBlockedErrorOperaciones afectadas
- Transcripción de mensajes de voz mediante el modelo
parakeeten puntos de conexión STT autoalojados - Cualquier invocación de la herramienta
media.audiodirigida a direcciones IP privadas/internas - Puntos de conexión de API compatibles con OpenAI en direcciones LAN (por ejemplo,
192.168.x.x,10.x.x.x,172.16.x.x)
Configuración que debería funcionar
La siguiente configuración es el enfoque documentado para habilitar el acceso a redes privadas para proveedores de 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" }]
}
}
}
}Contexto de versión
| Versión | Comportamiento |
|---|---|
| v2026.4.12 | ✅ Funciona correctamente |
| v2026.4.13 | ✅ Funciona correctamente |
| v2026.4.14 | ❌ Bloqueado por SSRF — regresión introducida |
Salida del comando de diagnóstico
Cuando se habilita la depuración, puede aparecer lo siguiente en los registros:
# 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🧠 Causa raíz
Descripción general
Dos errores independientes en la versión v2026.4.14 forman una cascada que elimina silenciosamente la configuración allowPrivateNetwork de la configuración de solicitudes del proveedor. Ambos errores deben estar presentes para reproducir la falla, y ambos deben corregirse para restaurar el comportamiento correcto.
Error 1: resolveProviderExecutionContext Elimina allowPrivateNetwork de la Configuración del Proveedor
Archivo afectado: runner.entries-*.js (archivo dist)
Ruta del código:
La función resolveProviderExecutionContext construye el objeto request pasado a transcribeAudio a través de la siguiente cadena de fusión:
javascript request: mergeProviderRequestOverrides( sanitizeConfiguredProviderRequest(params.config?.request), sanitizeConfiguredProviderRequest(params.entry.request) )
Causa raíz:
La función fusiona solo:
params.config?.request— configuración de solicitud a nivel de herramienta (desdetools.media.audio.request)params.entry.request— anulaciones de solicitud a nivel de entrada
Críticamente, la configuración a nivel de proveedor (models.providers.<id>.request) nunca se incluye en esta fusión. La función sanitizeConfiguredProviderRequest filtra explícitamente solo estos campos:
javascript // Fields preserved by sanitizeConfiguredProviderRequest const ALLOWED_REQUEST_FIELDS = [‘headers’, ‘auth’, ‘proxy’, ’tls’]; // Note: ‘allowPrivateNetwork’ is intentionally NOT in this list
Resultado: Incluso cuando un operador configura correctamente:
json “models”: { “providers”: { “openai”: { “request”: { “allowPrivateNetwork”: true } } } }
Este valor se descarta silenciosamente porque la configuración del proveedor nunca se consulta durante la construcción del objeto de solicitud para la transcripción de audio.
Error 2: resolveProviderRequestPolicyConfig Ignora allowPrivateNetwork en params.request
Archivo afectado: provider-request-config-*.js (archivo dist)
Ruta del código:
La función resolveProviderRequestPolicyConfig devuelve la política de seguridad resuelta:
javascript allowPrivateNetwork: params.allowPrivateNetwork ?? false
Causa raíz:
La función verifica solo params.allowPrivateNetwork — un parámetro directo que los llamadores deben pasar explícitamente. Sin embargo, todos los llamadores de transcripción de audio derivan su configuración de solicitud de resolveProviderHttpRequestConfig y la pasan como request: params.request.
Los llamadores (por ejemplo, transcribeOpenAiCompatibleAudio, transcribeDeepgramAudio) establecen:
javascript { request: resolveProviderHttpRequestConfig({ model: params.model, provider: params.provider, // allowPrivateNetwork IS present here from Bug 1 fix }) }
Pero resolveProviderRequestPolicyConfig recibe params.request?.allowPrivateNetwork y nunca lo verifica. El valor solo se lee del parámetro plano params.allowPrivateNetwork, que los llamadores de audio no establecen explícitamente.
Resultado: Incluso si el Error 1 se corrigiera y allowPrivateNetwork se colocara correctamente en params.request, el Error 2 lo descartaría silenciosamente durante la resolución de políticas.
Diagrama de Secuencia de Fallas
┌─────────────────────────────────────────────────────────────────────────┐ │ COMPORTAMIENTO CORRECTO (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 │ └─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐ │ COMPORTAMIENTO ROTO (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 │ └─────────────────────────────────────────────────────────────────────────┘
Por qué Se Requieren Ambos Errores
| ¿Error 1 corregido? | ¿Error 2 corregido? | Resultado |
|---|---|---|
| ❌ No | ❌ No | Bloqueo SSRF (estado actual roto) |
| ✅ Sí | ❌ No | Bloqueo SSRF (Error 2 todavía descarta el valor) |
| ❌ No | ✅ Sí | Sin cambio (Error 1 nunca propaga el valor) |
| ✅ Sí | ✅ Sí | ✅ Comportamiento correcto restaurado |
🛠️ Solución paso a paso
Opción 1: Hotfix Aplicado a Archivos Dist (Inmediato)
Este enfoque parcha los archivos de distribución compilados directamente. Adecuado para despliegues en contenedores o cuando reconstruir desde el código fuente no es inmediatamente posible.
Requisitos previos
- Acceso al sistema de archivos del contenedor en ejecución o al entorno de despliegue
- Los dos archivos dist afectados:
runner.entries-*.jsprovider-request-config-*.js
Paso 1: Localizar los Archivos Afectados
# 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-*.jsPaso 2: Parchar runner.entries-*.js (Corrección del Error 1)
Antes (Con error): javascript request: mergeProviderRequestOverrides( sanitizeConfiguredProviderRequest(params.config?.request), sanitizeConfiguredProviderRequest(params.entry.request) )
Después (Corregido): javascript request: mergeModelProviderRequestOverrides( sanitizeConfiguredModelProviderRequest(providerConfig?.request), sanitizeConfiguredProviderRequest(params.config?.request), sanitizeConfiguredProviderRequest(params.entry.request) )
Paso 3: Parchar provider-request-config-*.js (Corrección del Error 2)
Antes (Con error): javascript allowPrivateNetwork: params.allowPrivateNetwork ?? false
Después (Corregido): javascript allowPrivateNetwork: params.allowPrivateNetwork ?? params.request?.allowPrivateNetwork ?? false
Paso 4: Reiniciar la Aplicación
# For Docker containers
docker-compose restart openclaw
# For Kubernetes
kubectl rollout restart deployment/openclaw
# For systemd
sudo systemctl restart openclawOpción 2: Workaround de Configuración (Sin Cambios en Código)
Si no puede modificar los archivos dist inmediatamente, puede evitar el error especificando allowPrivateNetwork en la configuración a nivel de herramienta en lugar del nivel de proveedor.
Cambio de Configuración
Antes (Nivel de proveedor — No funciona en 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” }] } } } }
Después (Workaround a nivel de herramienta): json { “models”: { “providers”: { “openai”: { “baseUrl”: “http://192.168.x.x:5092/v1" } } }, “tools”: { “media”: { “audio”: { “request”: { “allowPrivateNetwork”: true }, “models”: [{ “provider”: “openai”, “model”: “parakeet” }] } } } }
Nota: Este workaround coloca allowPrivateNetwork en tools.media.audio.request, que se verifica a través de params.config?.request en la cadena de fusión existente. Sin embargo, esto debe aplicarse a cada configuración de herramienta que necesite acceso a la red privada.
Opción 3: Corrección Permanente mediante Modificación del Código Fuente (Recomendado)
Para una resolución a largo plazo, aplique las correcciones a los archivos TypeScript de origen antes de compilar.
Corrección del Error 1 — Archivo de Origen
Archivo: src/api/worker/runner/entries.ts (o equivalente)
Cambio: 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) );
Corrección del Error 2 — Archivo de Origen
Archivo: src/api/providers/provider-request-config.ts (o equivalente)
Cambio: typescript // Before const allowPrivateNetwork = params.allowPrivateNetwork ?? false;
// After const allowPrivateNetwork = params.allowPrivateNetwork ?? params.request?.allowPrivateNetwork ?? false;
Recompilar y Desplegar
# 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🧪 Verificación
Requisitos previos para la verificación
Asegúrese de tener:
- Un punto de conexión STT autoalojado en una IP privada (por ejemplo, Parakeet en
192.168.1.100:5092) - Un archivo de audio de prueba para transcripción
- Acceso a los registros de despliegue
Paso 1: Verificar que la Configuración Está Cargada
Verifique que su configuración de proveedor con allowPrivateNetwork esté correctamente reconocida:
# 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 } }Paso 2: Habilitar el Registro de Depuración de Seguridad
# Set debug environment variable
export DEBUG=openclaw:security:*
# Or in docker-compose.yml:
# environment:
# - DEBUG=openclaw:security:*Paso 3: Ejecutar Transcripción de Prueba
# 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"
}'Paso 4: Verificar Éxito (Comportamiento Corregido)
Salida esperada (Éxito):
[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 errorVerificación de Registro de Depuración (Corregido):
[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 falsePaso 5: Verificar Estado de Falla (Línea base)
Si la corrección no está aplicada, verá:
[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 configPaso 6: Suite de Pruebas de Regresión
Cree un script de prueba para verificar ambos escenarios:
#!/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 ==="⚠️ Errores comunes
1. Malinterpretar la Jerarquía de Configuración
Error: Los operadores colocan allowPrivateNetwork en la ubicación incorrecta, esperando que se propague automáticamente.
Detalles:
models.providers.<id>.request.allowPrivateNetwork— nivel de proveedor (roto en v2026.4.14)tools.media.audio.request.allowPrivateNetwork— nivel de herramienta (funciona, pero debe establecerse explícitamente por herramienta)tools.*.request.allowPrivateNetwork— comodín (solo afecta las herramientas coincidentes)
Enfoque correcto: Hasta que se corrija el error, siempre establezca allowPrivateNetwork a nivel de herramienta para transcripción de audio.
2. Asumir que la Validación de Esquema Pasa Cuando la Configuración Es Ignorada
Error: El esquema valida allowPrivateNetwork como un campo válido, por lo que los operadores asumen que está siendo utilizado.
Detalles: El esquema JSON para models.providers.*.request incluye allowPrivateNetwork (agregado en #63671 para v2026.4.12). Sin embargo, la ruta del código nunca lee este campo para transcripción de audio. Esto crea un falso positivo — la configuración parece válida pero se descarta silenciosamente.
Workaround: Siempre verifique el comportamiento con solicitudes de prueba reales, no solo con validación de esquema.
3. Conflictos de Montaje de Volúmenes Docker
Error: Cuando se usan montajes de volumen para parchar archivos dist, los archivos originales pueden restaurarse al reiniciar el contenedor.
Detalles: Si parchear archivos dist/ directamente en un contenedor:
yaml
docker-compose.yml
volumes:
- ./patched-runner.entries.js:/app/dist/api/worker/runner.entries-abc123.js
La política de reinicio del contenedor o la reconstrucción de la imagen sobrescribirán su parche.
Solución: Use una imagen derivada con el parche incluido: 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. Retraso en la Propagación de Caché
Error: Después de corregir la configuración, la transcripción de audio todavía falla debido a la resolución de proveedor en caché.
Detalles: OpenClaw almacena en caché las configuraciones de proveedor resueltas. Una corrección a models.providers.<id>.request.allowPrivateNetwork puede no tomar efecto hasta que:
- El TTL del caché expire
- La aplicación se reinicie
- El caché se limpie explícitamente
Comandos: bash
Clear configuration cache
rm -rf ~/.openclaw/cache/* rm -rf /tmp/openclaw-*
Or restart the service
systemctl restart openclaw
5. Políticas de Seguridad en Conflicto
Error: Incluso con allowPrivateNetwork: true, una política de seguridad global puede sobrescribirla.
Detalles: Verifique si hay configuraciones en conflicto: json { “security”: { “networkPolicy”: { “allowPrivate”: false // This would override provider-level settings } } }
Verificación: bash openclaw config show –path security
6. Direcciones Privadas IPv6
Error: allowPrivateNetwork puede no cubrir todos los rangos de direcciones IPv6 privadas.
Detalles: Las siguientes pueden seguir siendo bloqueadas incluso con allowPrivateNetwork: true:
::1(loopback)fc00::/7(unique local addresses)fe80::/10(link-local addresses)
Solución: Si usa direcciones IPv6 privadas, verifique que la política SSRF las incluya explícitamente: bash DEBUG=openclaw:security:* openclaw media transcribe … 2>&1 | grep -i “ipv6|private”
7. Conflictos de Verificación TLS/SSL
Error: Los puntos de conexión de red privada a menudo usan certificados autofirmados. Si request.tls.rejectUnauthorized no está configurado, las solicitudes pueden fallar con errores de certificado.
Detalles: Una configuración completa de red privada debe incluir: json { “models”: { “providers”: { “openai”: { “baseUrl”: “https://192.168.1.100:5092/v1”, “request”: { “allowPrivateNetwork”: true, “tls”: { “rejectUnauthorized”: false } } } } } }
🔗 Errores relacionados
Errores directamente relacionados
SsrFBlockedError— El error principal visto en esta regresión. Ocurre cuando la política SSRF bloquea una solicitud a un nombre de host o dirección IP no explícitamente permitida.[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— Fallo general de captura de URL, puede ocurrir si SSRF no es el mecanismo de bloqueo pero la URL es inválida de otra manera.ProviderRequestConfigError— Se lanza cuando la configuración de solicitud no se puede resolver. Puede ocurrir si la cadena de fusión encuentra valores indefinidos.
Problemas históricamente relacionados
PR #63671 (v2026.4.12) — Introdujo el esquema
models.providers.*.request.allowPrivateNetworky la implementación inicial. Este PR agregó la capacidad que la v2026.4.14 hace retroceder.Issue #64201 — SSRF bypass via redirect — Preocupación histórica sobre permitir acceso a redes privadas. Cualquier corrección para la regresión actual debe asegurar que los objetivos de redirección también estén sujetos a verificaciones de
allowPrivateNetwork.Issue #63847 — Media transcription timeout on large files — Relacionado con la confiabilidad de transcripción de audio; puede compartir rutas de manejo de errores con esta regresión.
Issue #64012 — Provider config merge priority unclear — Documenta la confusión sobre qué nivel de configuración toma precedencia. Relevante porque el Error 1 proviene de la configuración del proveedor no incluida en la fusión.
Patrones de Error Similares
| Código de error | Descripción | Distinción |
|---|---|---|
NetworkError | Fallo de red genérico | Sin componente SSRF; generalmente DNS/conexión rechazada |
CertificateError | Fallo de validación TLS/SSL | Relacionado con certificados autofirmados de red privada |
TimeoutError | Tiempo de espera de solicitud | Puede ser mal diagnosticado si SSRF bloquea rápido |
AuthenticationError | Fallo de autenticación | Sin relación; indica credenciales incorrectas |
Referencia de Depuración
Al reportar problemas relacionados con esta regresión, incluya:
# 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)"