Skip to content

SSRF in connection post-connection scripts — egress policy not applied in internal-nango (incomplete fix of #6646)

High
rossmcewan published GHSA-fqr5-mf8g-p5m2 Aug 26, 2026

Package

nangohq/nango (self-hosted server & Nango Cloud)

Affected versions

before commit 688df05f3e15aa0e5ddca632a40c4fb06f294a38

Patched versions

688df05f3e15aa0e5ddca632a40c4fb06f294a38

Description

Summary

The SSRF egress guard added in PR #6646 ("fix(egress): outbound url policy across all customer-controlled egress paths", commit 18ddc621b) is not applied to the ProxyRequest that runs provider post-connection and pre-connection-deletion scripts (packages/server/lib/hooks/connection/internal-nango.ts). That ProxyRequest is constructed without outboundPolicy, while the sibling credentials-verification-script.ts — updated by that same PR — passes outboundPolicy: getServerOutboundUrlPolicy(). On this path there is no hostname denylist, no private/link-local IP classification, no DNS-pinning, and no redirect re-validation. This is an incomplete fix of #6646.

Severity

High — CVSS 3.1 8.5 AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N (CWE-918). Conservative floor 6.3 (Medium) if assessed as strictly blind.

Affected

All versions through 0.71.4 (the #6646 egress guard, shipped ~0.71.0, was never extended to this path). Verified on the official nangohq/nango-server:hosted image, version 0.71.4.

Details (root cause)

// internal-nango.ts — VULNERABLE (no outboundPolicy)
const proxyInstance = new ProxyRequest({ logger, proxyConfig: proxyConfigUnwrapped,
                                         getConnection, getIntegrationConfig, onBytes });

// credentials-verification-script.ts:112 — sibling, guarded (updated by #6646)
const proxyInstance = new ProxyRequest({ ..., outboundPolicy: getServerOutboundUrlPolicy() });

In packages/shared/lib/services/proxy/utils.ts (getAxiosConfiguration) every protection is gated on outboundPolicy being present: assertSafeOutboundUrlSync (denylist + IP classification), the safe DNS lookup (anti-rebind), maxRedirects, and the beforeRedirect re-validation. With outboundPolicy undefined the request uses a default axios agent — no guard — and follows the default 5 redirects unchecked, including cross-scheme https → http. The second guard, enforceProxyOutboundUrlPolicy, is only called from proxy.service.ts and the runner SDK, not this path.

PoC

An authenticated tenant (an environment secret key from self-serve signup) triggers it via POST /connections. The snowflake provider's proxy.base_url is https://${connectionConfig.snowflake_account_url} (attacker-controlled host), and OAUTH2 import performs no token exchange, so a dummy access_token is accepted — no real Snowflake account or OAuth app needed:

POST /connections
Authorization: Bearer <environment secret key>
Content-Type: application/json

{"provider_config_key":"snowflake","connection_id":"poc",
 "credentials":{"type":"OAUTH2","access_token":"dummy"},
 "connection_config":{"snowflake_account_url":"<attacker-controlled host>"}}

The connection-created hook runs the snowflake post-connection script through the unguarded ProxyRequest, so the backend issues POST https://<attacker host>/api/v2/statements to any internal / private / cloud-metadata address. Because redirects are followed unchecked, a first-hop HTTPS host the attacker owns (any host with a valid public certificate) can 307 the backend to a plaintext http://<internal> target (method + body preserved). The snowflake script stores response.data.data[0][0] into connection_config.username, so for internal endpoints returning Snowflake-shaped JSON the response is exfiltrated back to the tenant via GET /connections (non-blind); for other shapes the SSRF is blind (reach only). Confirmed end-to-end against v0.71.4, including the cross-scheme redirect with a real (non-lab) public host + valid certificate and the response reflected into username.

Impact

Authenticated (tenant secret key) SSRF from Nango's multi-tenant backend to arbitrary internal hosts/ports and cloud-metadata endpoints (reach), with the entire egress guard bypassed and cross-scheme redirects followed; attacker-directed POST (method + body preserved) to internal endpoints; and non-blind exfiltration of the response for the Snowflake-shaped class.

The gap is systemic to the path: all 43 providers with a post/pre-connection script run through the same unguarded ProxyRequest. Audited against providers.yaml, snowflake is the one provider today whose script issues an outbound call to a fully attacker-controlled host (others are no-ops, mutate local config, hard-code baseUrlOverride to the vendor host, or are bound to a vendor subdomain/path). Any future provider or connection_config change that lets a script's outbound host be influenced inherits the SSRF automatically. Integration-level custom.baseUrl does not reach this path (only integration_config providers use it, none of which has a post/pre-connection script, and internal-nango's getIntegrationConfig returns only OAuth credentials, no custom).

Remediation

Pass outboundPolicy: getServerOutboundUrlPolicy() in internal-nango.ts, mirroring credentials-verification-script.ts. Defense in depth: make outboundPolicy a required argument of ProxyRequest (or add a lint/type guard) so no outbound proxy can be built without it, with a test asserting every construction site carries a policy.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Changed
Confidentiality
High
Integrity
Low
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N

CVE ID

No known CVE

Weaknesses

Server-Side Request Forgery (SSRF)

The web server receives a URL or similar request from an upstream component and retrieves the contents of this URL, but it does not sufficiently ensure that the request is being sent to the expected destination. Learn more on MITRE.

Credits