# GHSA-8q49-2h5h-434x: FrontMCP: Server-Side Request Forgery (SSRF) in the OpenAPI adapter spec-change poller

> An SSRF vulnerability in the OpenAPI adapter's spec poller allows attackers to access internal resources when polling is enabled against untrusted URLs.

- **Severity:** Not scored
- **Exploited in the wild:** Not known
- **Login needed:** Not needed
- **Attackable:** Over the internet
- **Victim action needed:** Not needed
- **Fix available:** Yes
- **Published:** 24 Jul 2026

## What is GHSA-8q49-2h5h-434x?

GHSA-8q49-2h5h-434x is a server-side request forgery vulnerability in @frontmcp/adapters, published on 24 Jul 2026. An attacker can exploit it over the internet without logging in, with no action from the victim.

### Official description

## Summary

The 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.

## Details

The initial spec load is guarded. `OpenapiAdapter` resolves a secure `refResolution`
policy and passes it to the guarded loader:

```ts
// libs/adapters/src/openapi/openapi.adapter.ts — initializeGenerator()
return await OpenAPIToolGenerator.fromURL(this.options.url, {
  // ...
  followRedirects: this.options.loadOptions?.followRedirects ?? false,
  refResolution, // secure default: external $refs off, internal targets blocked
});
```

But the poller — which re-fetches **the same URL** on every interval — did not:

```ts
// libs/adapters/src/openapi/openapi-spec-poller.ts — doFetch() (vulnerable, <= 1.5.5)
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), this.fetchTimeoutMs);
try {
  const response = await fetch(this.url, {   // <-- raw global fetch, no SSRF guard
    headers,
    signal: controller.signal,
  });
  // ...hash the body, fire onChanged...
}
```

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.

## Impact

A 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.

## Preconditions

Exploitation requires **both**:

1. `polling.enabled: true` on an `OpenapiAdapter` (polling is off by default and
   requires the URL-based `url` option, not an inline `spec`); **and**
2. 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 concept

```ts
import { OpenapiAdapter } from '@frontmcp/adapters';

// url is attacker-influenceable and points (directly, via DNS, or via redirect)
// at an internal target; polling re-fetches it every interval.
const adapter = OpenapiAdapter.init({
  name: 'evil',
  url: 'http://169.254.169.254/latest/meta-data/', // or http://127.0.0.1.nip.io/...
  polling: { enabled: true, intervalMs: 5000 },
});

await adapter.fetch();   // initial load IS guarded (blocked)
adapter.startPolling();  // <= 1.5.5: each poll issues an UNGUARDED GET to the internal target
```

On `<= 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.

## Patch

The 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.

## Remediation

Upgrade `@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.

## Workarounds

For users who cannot upgrade immediately:

- disable polling (`polling.enabled: false`) on adapters whose spec `url` is not a
  fixed, trusted, first-party value; or
- only enable polling against spec URLs you fully control, served over HTTPS from a
  host that cannot be made to redirect to internal targets; and
- enforce network egress controls / an allow-list at the platform layer so the server
  cannot reach internal ranges or cloud-metadata endpoints.

## Which versions are affected by GHSA-8q49-2h5h-434x?

2 open source packages ship an affected version, including @frontmcp/adapters.


| Package | Ecosystem | Affected | Fixed in |
| --- | --- | --- | --- |
| `@frontmcp/adapters` | npm | All versions | 1.5.6 |
| `@frontmcp/adapters` | npm | All versions | 1.5.6 |

## How do I fix GHSA-8q49-2h5h-434x?

Upgrade @frontmcp/adapters to 1.5.6 or later.

## Is GHSA-8q49-2h5h-434x being exploited?

There is no known exploitation of GHSA-8q49-2h5h-434x in the wild and no public exploit code.

## Fix prompt for AI coding agents

```text
Check this codebase for GHSA-8q49-2h5h-434x and fix it if it is affected.

What it is: GHSA-8q49-2h5h-434x is a server-side request forgery vulnerability in @frontmcp/adapters, published on 24 Jul 2026. An attacker can exploit it over the internet without logging in, with no action from the victim.
Exploitation: There is no known exploitation of GHSA-8q49-2h5h-434x in the wild and no public exploit code.

Affected packages and fixed versions:
- @frontmcp/adapters (npm): affected all versions; fixed in 1.5.6
- @frontmcp/adapters (npm): affected all versions; fixed in 1.5.6

Task:
1. Search the dependency manifests and lockfiles (for example package.json, package-lock.json, requirements.txt, poetry.lock, uv.lock, pom.xml, build.gradle, go.mod, Cargo.lock, Gemfile.lock) and any Dockerfiles for the affected packages, including transitive dependencies.
2. If an affected version is in use, upgrade it to the fixed version for its release line with the project's package manager, so the lockfile is regenerated. Prefer the smallest upgrade that fixes it.
3. If there is no fix, or the upgrade would break the build, do not guess: explain the risk and the safest workaround instead.
4. Run the tests and summarise what changed and why.
5. If nothing in the project is affected, say so and change nothing.

Reference: https://insights.sbomify.com/GHSA-8q49-2h5h-434x/ (Markdown: https://insights.sbomify.com/GHSA-8q49-2h5h-434x/index.md)
```

## Sources

- OSV: https://osv.dev/vulnerability/GHSA-8q49-2h5h-434x

---
From sbomify Insights: https://insights.sbomify.com/GHSA-8q49-2h5h-434x/
Summaries are generated and may contain mistakes; the official description above is the record.
