---
id: GHSA-8q49-2h5h-434x
title: >-
  FrontMCP: Server-Side Request Forgery (SSRF) in the OpenAPI adapter
  spec-change poller
summary: >-
  FrontMCP: Server-Side Request Forgery (SSRF) in the OpenAPI adapter
  spec-change poller
severity: medium
cvss: 5.9
cwe:
  - CWE-918
vendor: frontmcp
product: '@frontmcp/adapters'
ecosystem: npm
affected:
  - '@frontmcp/adapters <= 1.5.5'
patched:
  - '@frontmcp/adapters 1.5.6'
published: '2026-07-24'
updated: '2026-07-24'
source: GHSA
sourceUrl: 'https://github.com/advisories/GHSA-8q49-2h5h-434x'
references:
  - url: >-
      https://github.com/agentfront/frontmcp/security/advisories/GHSA-8q49-2h5h-434x
  - url: 'https://github.com/agentfront/frontmcp/pull/510'
  - url: >-
      https://github.com/agentfront/frontmcp/commit/077201e109bf6f45dbc85c36d6bd77ded18ab13e
  - url: 'https://github.com/agentfront/frontmcp/releases/tag/v1.5.6'
  - url: 'https://github.com/advisories/GHSA-8q49-2h5h-434x'
tags:
  - ghsa
  - npm
ingestedAt: '2026-07-24T23:41:39.449Z'
---

## Overview

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

## Affected packages

- `@frontmcp/adapters <= 1.5.5`

## Remediation

Upgrade to a patched release:

- `@frontmcp/adapters 1.5.6`
