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.
The initial spec load is guarded. OpenapiAdapter resolves a secure refResolution
policy and passes it to the guarded loader:
// 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:
// 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:
allowedHosts / blockedHosts);http://127.0.0.1.nip.io/, was reached);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.
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:
169.254.169.254) — credential /
token theft;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.
Exploitation requires both:
polling.enabled: true on an OpenapiAdapter (polling is off by default and
requires the URL-based url option, not an inline spec); andurl 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.
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.
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.tslibs/adapters/src/openapi/openapi-spec-poller.types.tslibs/adapters/src/openapi/openapi.adapter.tsRequires 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.
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.
For users who cannot upgrade immediately:
polling.enabled: false) on adapters whose spec url is not a
fixed, trusted, first-party value; or| Software | From | Fixed in |
|---|---|---|
@frontmcp / adapters
|
- | 1.5.6 |
A security vulnerability is a weakness in software, hardware, or configuration that can be exploited to compromise confidentiality, integrity, or availability. Many vulnerabilities are tracked as CVEs (Common Vulnerabilities and Exposures), which provide a standardized identifier so teams can coordinate patching, mitigation, and risk assessment across tools and vendors.
CVSS (Common Vulnerability Scoring System) estimates technical severity, but it doesn't automatically equal business risk. Prioritize using context like internet exposure, affected asset criticality, known exploitation (proof-of-concept or in-the-wild), and whether compensating controls exist. A "Medium" CVSS on an exposed, production system can be more urgent than a "Critical" on an isolated, non-production host.
A vulnerability is the underlying weakness. An exploit is the method or code used to take advantage of it. A zero-day is a vulnerability that is unknown to the vendor or has no publicly available fix when attackers begin using it. In practice, risk increases sharply when exploitation becomes reliable or widespread.
Recurring findings usually come from incomplete Asset Discovery, inconsistent patch management, inherited images, and configuration drift. In modern environments, you also need to watch the software supply chain: dependencies, containers, build pipelines, and third-party services can reintroduce the same weakness even after you patch a single host. Unknown or unmanaged assets (often called Shadow IT) are a common reason the same issues resurface.
Use a simple, repeatable triage model: focus first on externally exposed assets, high-value systems (identity, VPN, email, production), vulnerabilities with known exploits, and issues that enable remote code execution or privilege escalation. Then enforce patch SLAs and track progress using consistent metrics so remediation is steady, not reactive.
SynScan combines attack surface monitoring and continuous security auditing to keep your inventory current, flag high-impact vulnerabilities early, and help you turn raw findings into a practical remediation plan.