CVE Patch Review

GHSA-8Q49-2H5H-434X: Poller SSRF Guard Restored in FrontMCP

GHSA-8Q49-2H5H-434X · Updated 2026-07-25 Root-cause

Summary

FrontMCP's OpenAPI spec poller previously bypassed the SSRF protections used during the initial spec load by calling the global fetch() directly. The patch routes poll-time retrieval through mcp-from-openapi's safeFetch(), propagates normalized SSRF policy and redirect behavior from adapter configuration, and adds end-to-end tests covering default loopback blocking, explicit internal-IP opt-in, and adapter-level error surfacing. Based on the provided diff, this is a root-cause fix for the poller-specific SSRF gap.

Analysis

Vulnerability

GHSA-8Q49-2H5H-434X describes a server-side request forgery condition in the FrontMCP OpenAPI spec poller. The vulnerable path was not the initial OpenAPI load, but the background re-fetch logic used for polling. The initial load already relied on SSRF-aware logic in mcp-from-openapi, while the poller performed a direct global fetch(this.url, ...) with only timeout handling. That created a policy gap: an attacker-influenceable spec URL could be re-polled without resolved-IP validation, connection pinning, or redirect re-validation.

The diff shows the vulnerable implementation in libs/adapters/src/openapi/openapi-spec-poller.ts using raw fetch() against this.url. Because polling re-fetches the same URL on a timer, this bypass enabled SSRF against internal interfaces, including scenarios involving DNS rebinding or redirect chains, exactly as summarized in the advisory context. The issue is therefore architectural inconsistency: the same trust boundary was enforced during initial load but not during subsequent poll cycles.

// vulnerable poll path
const response = await fetch(this.url, {
  headers,
  signal: controller.signal,
});

Relevant sources: pull request #510, fix commit 077201e, and the GitHub Security Advisory.

Patch

The patch replaces the raw network primitive in the poller with the same guarded fetch path used by the initial OpenAPI load. In libs/adapters/src/openapi/openapi-spec-poller.ts, the code now imports normalizeSsrfOptions, safeFetch, and ResolvedSsrfOptions from mcp-from-openapi. The poller stores normalized SSRF settings and a redirect policy, defaulting redirects to false, then calls safeFetch() with timeoutMs, followRedirects, and ssrf.

const response = await safeFetch(this.url, {
  headers,
  timeoutMs: this.fetchTimeoutMs,
  followRedirects: this.followRedirects,
  ssrf: this.ssrf,
});

The adapter wiring is also important. In libs/adapters/src/openapi/openapi.adapter.ts, polling options are rebuilt so the poller inherits the same SSRF policy derived from loadOptions.refResolution via normalizeSsrfOptions(this.resolveRefResolution()). Redirect behavior is likewise propagated from this.options.loadOptions?.followRedirects ?? false. This closes the configuration drift between initial load and poll-time refresh.

The patch extends the poller types to explicitly carry ssrf?: ResolvedSsrfOptions and followRedirects?: boolean, documenting that polling defaults to blocking internal/private addresses and that redirect hops are re-validated when enabled. The test suite is strengthened in libs/adapters/src/openapi/__tests__/openapi-polling-e2e.spec.ts by preserving the real SSRF guard instead of fully mocking mcp-from-openapi. New tests verify that loopback URLs fail closed by default, internal polling works only with explicit allowInternalIPs opt-in, and adapter-level polling surfaces SSRF blocks without emitting updates. The docs were updated consistently in the adapter and polling guides to explain the inherited SSRF policy and the trusted-only nature of allowInternalIPs.

Review

Pros

  • The patch addresses the actual trust-boundary mismatch by removing the raw fetch() from the poller and reusing the hardened safeFetch() path already established for initial spec loading, as shown in the fix commit.
  • SSRF policy is propagated from adapter configuration into polling via normalizeSsrfOptions(this.resolveRefResolution()), which reduces the chance of future divergence between initial load and refresh behavior.
  • Redirect handling is explicitly controlled and defaults to false, with documentation stating that redirect hops are re-validated. That is directly relevant to the advisory's redirect-based exploitation angle.
  • The tests are materially improved: they stop over-mocking the SSRF layer and add negative and positive coverage for loopback blocking, explicit internal-IP opt-in, and adapter-level logging/error behavior. This is stronger than a unit-only assertion because it exercises the real guarded fetch path.
  • Documentation updates are aligned with the code changes and clearly state fail-closed behavior, default blocking of loopback/private/internal targets, and the risk of enabling allowInternalIPs for untrusted URLs.

Cons

  • The reviewable evidence is limited to the provided diff and snippets; the security properties of the fix still depend on the correctness of mcp-from-openapi's safeFetch() and normalizeSsrfOptions(), which are referenced but not expanded in the supplied material.
  • The patch appears scoped to the OpenAPI spec poller path only. That is appropriate for this advisory, but it does not by itself prove that no other background refresh or secondary fetch paths in the codebase still use raw network primitives.
  • The new standalone poller option ssrf?: ResolvedSsrfOptions is powerful; if downstream callers construct permissive policies directly, they can still opt themselves into risky behavior. The docs warn about this, but the API remains security-sensitive.

Verdict

Root-cause.

The vulnerable behavior existed because the poller used a separate, unguarded network path for the same attacker-influenceable URL that was already protected during initial load. The patch removes that bypass by routing poll-time fetches through the same SSRF enforcement mechanism, carrying over normalized SSRF policy and redirect settings from the adapter. The added tests specifically validate the previously missing security invariant: polling must fail closed on internal targets unless explicitly opted in. Based on the supplied sources, this is not merely input filtering or logging around the old behavior; it is a direct replacement of the unsafe primitive with the intended guarded primitive.

Sources: https://github.com/agentfront/frontmcp/pull/510, https://github.com/agentfront/frontmcp/commit/077201e109bf6f45dbc85c36d6bd77ded18ab13e, https://github.com/advisories/GHSA-8Q49-2H5H-434X, https://cvereports.com/reports/GHSA-8Q49-2H5H-434X.

Sources