GHSA-8q49-2h5h-434x FrontMCP: Server-Side Request Forgery (SSRF) in the OpenAPI adapter spec-change poller
SummaryThe OpenAPI adapter's spec-change **poller** (`OpenApiSpecPoller`) re-fetched the
configured spec `url` on a timer using a raw global `fetch()`, bypassing the SSRF
guard (`safeFetch` / `assertUrlSafe`) that `OpenAPIToolGenerator.fromURL()` applies
to the initial spec load. As a result, the pinning/DNS-resolution hardening delivered
via `mcp-from-openapi >= 2.5.0` (advisory GHSA-65h7-9wrw-629c) protected the initial
load but **not** the recurring poll of the same URL. When polling is enabled against
an untrusted or attacker-influenceable spec URL, this is an unguarded SSRF vector.DetailsThe initial spec load is guarded. `OpenapiAdapter` resolves a secure `refResolution`
policy and passes it to the guarded loader:But the poller — which re-fetches **the same URL** on every interval — did not:Because `doFetch()` never called `safeFetch`, none of the guard's protections applied
to the polled request:no allow-list / block-list enforcement (`allowedHosts` / `blockedHosts`);no internal/private/loopback/link-local/CGNAT/cloud-metadata IP blocking;no DNS resolution of the hostname (so a DNS name that resolves to an internal IP,
e.g. `http://127.0.0.1.nip.io/`, was reached);no connection **pinning** to the validated IP (DNS-rebinding TOCTOU);no per-hop re-validation of HTTP redirects.This is the identical threat model to `fromURL()` / external `$ref` resolution
(GHSA-65h7-9wrw-629c), applied to a request path that the fix for that advisory did
not cover.ImpactA server that enables spec polling against an untrusted or attacker-influenceable
spec URL will, on every poll interval, issue a server-side `GET` to whatever host the
URL (or a DNS name it resolves to, or a redirect it returns) points at — including
internal-only addresses unreachable from the public internet. Consequences include:reading cloud-instance metadata endpoints (e.g. `169.254.169.254`) — credential /
token theft;probing and reaching internal services and private-range hosts (internal network
scanning);DNS-rebinding to swap a public host for an internal one between validation and
connection.The poller issues `GET` requests only, so the primary impact is **confidentiality**
(reaching and reading internal endpoints); the fetched body is content-hashed to
detect change and the subsequent tool rebuild goes back through the guarded
`fromURL()` path.PreconditionsExploitation requires **both**:`polling.enabled: true` on an `OpenapiAdapter` (polling is off by default and
requires the URL-based `url` option, not an inline `spec`); **and**the spec `url` is untrusted / attacker-influenceable (e.g. it is derived from user
input, a tenant-supplied value, or otherwise not a fixed trusted constant), or an
otherwise-trusted spec host is attacker-controlled or can redirect.Servers that poll a fixed, trusted, first-party spec URL are not exposed in practice,
though they still benefit from the guard as defense-in-depth.Proof of conceptOn `<= 1.5.5` the timed poll reaches the internal address. On the patched version the
poll fails closed (no request is made; the failure is logged) exactly as the initial
load does.PatchThe fix routes the poller through the same SSRF guard as the initial load, with the
same policy, so both paths share one DNS resolution + connection pinning and cannot
diverge:`OpenApiSpecPoller.doFetch()` now calls `safeFetch(this.url, { headers, timeoutMs,
followRedirects, ssrf })` from `mcp-from-openapi` instead of the global `fetch()`.`OpenapiAdapter.startPolling()` injects the adapter's resolved policy into the
poller: `ssrf: normalizeSsrfOptions(this.resolveRefResolution())` and
`followRedirects: loadOptions?.followRedirects ?? false` — identical to what
`fromURL()` receives.`SpecPollerOptions` gained optional `ssrf` / `followRedirects`; standalone use of
`OpenApiSpecPoller` defaults to the secure policy (internal targets blocked,
redirects not followed).Files changed:`libs/adapters/src/openapi/openapi-spec-poller.ts``libs/adapters/src/openapi/openapi-spec-poller.types.ts``libs/adapters/src/openapi/openapi.adapter.ts`Requires `mcp-from-openapi >= 2.5.0` (already a dependency at `2.5.1`), which exports
`safeFetch` / `normalizeSsrfOptions` and performs the resolved-IP validation and
connection pinning.RemediationUpgrade `@frontmcp/adapters` to `1.5.6` or later. No configuration change is required:
polling now inherits the same secure defaults as the initial spec load (external
targets blocked, redirects not followed). To poll a genuinely internal or localhost
spec server in a trusted environment, opt in explicitly with
`loadOptions.refResolution.allowInternalIPs: true` — the same knob that gates the
initial load.WorkaroundsFor users who cannot upgrade immediately:disable polling (`polling.enabled: false`) on adapters whose spec `url` is not a
fixed, trusted, first-party value; oronly enable polling against spec URLs you fully control, served over HTTPS from a
host that cannot be made to redirect to internal targets; andenforce network egress controls / an allow-list at the platform layer so the server
cannot reach internal ranges or cloud-metadata endpoints.
CVSS v3.1 score: 5.9