Commit bc5bf8f
fix(openapi-mcp-server): DNS-pin spec fetches to prevent rebinding SSRF (#4108)
* fix(openapi-mcp-server): DNS-pin spec fetches to prevent rebinding SSRF
The multi-spec (additional_specs) path validated spec_url via
validate_url_for_spec() and then discarded the result, re-fetching with
httpx.get(url) — a fresh DNS resolution. A rebinding server could answer a
public IP during validation and 169.254.169.254 (or any internal IP) at
fetch time (CWE-350 / CWE-367 TOCTOU SSRF). The primary spec URL was not
validated at all.
Fetch the spec by connecting only to the IP(s) validation already pinned,
never re-resolving the hostname:
- utils/openapi.py: add _pinned_fetch() — dials the pinned IP literal with
Host + sni_hostname set to the validated hostname, follow_redirects=False
with explicit 30x rejection, and a 10 MiB streamed size cap (the old
loader read bodies unbounded). load_openapi_spec() gains a validated_url
param and validates+pins a bare url in-function via _validate_url_sync(),
so no code path fetches a spec un-pinned. SSRFError/SSRFFetchError are not
retried away.
- server.py: thread the ValidatedURL from validation into load_openapi_spec()
for additional specs; the primary spec is validated+pinned by the loader
with the --allow-* flags passed through.
fastmcp's ssrf_safe_fetch is intentionally not reused: it re-resolves DNS,
is HTTPS-only, caps at 5 KB, and is async (would crash the sync loader inside
the running event loop).
Tests cover rebinding (DNS resolved once; metadata IP never dialed), redirect
refusal, mixed public/private rejection, size caps, http opt-in, and the
newly-validated primary path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(openapi-mcp-server): document DNS pinning; add coverage for pinned fetch
- README: replace the outdated "deploy behind an egress proxy for full DNS
pinning" note with a DNS-pinning (rebinding protection) section; note the
10 MiB cap and redirect refusal; clarify --allow-insecure-http is still pinned.
- CHANGELOG: add Unreleased Security entry for the DNS-rebinding TOCTOU fix.
- tests: cover the new branches — scheme rejection, retry-across-pinned-IPs,
all-IPs-fail, no-IPs, the running-loop sync bridge, http opt-in (no SNI),
and that SSRF failures are not retried.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(openapi-mcp-server): cover _parse_spec_bytes paths for patch coverage
Add unit tests for the prance-success, prance-failure→JSON, and
JSON-failure→YAML branches of _parse_spec_bytes so codecov/patch reflects
the new fetch/parse helpers (utils/openapi.py 89%→93%).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: pull request finding 'Explicit returns mixed with implicit (fall through) returns'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* fix: finding 'Empty except'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
* fix(openapi-mcp-server): correct misplaced terminal raise in load_openapi_spec
The autofix for the implicit-None fall-through inserted the terminal
`raise ValueError` directly after the docstring, making it unconditional
and killing every code path (url, path, validated_url) before any real
logic ran. Move the terminal raise to the true fall-through point at the
end of the function; the all-empty guard and the return/raise in each
branch remain intact.
Also fix test_path_invalid_validation, which patched
validate_openapi_spec in the module where it is defined
(openapi_validator) rather than where openapi.py binds it at import
time. The mock never took effect, so the test failed independently of
this change (fails on main too). Patch the used name to match the
passing sibling test_url_invalid_spec.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>1 parent 95623c9 commit bc5bf8f
9 files changed
Lines changed: 687 additions & 81 deletions
File tree
- src/openapi-mcp-server
- awslabs/openapi_mcp_server
- utils
- tests
- utils
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
5 | 5 | | |
6 | 6 | | |
7 | 7 | | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
8 | 16 | | |
9 | 17 | | |
10 | 18 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
329 | 329 | | |
330 | 330 | | |
331 | 331 | | |
| 332 | + | |
| 333 | + | |
332 | 334 | | |
333 | 335 | | |
334 | 336 | | |
335 | 337 | | |
336 | 338 | | |
337 | 339 | | |
338 | | - | |
| 340 | + | |
339 | 341 | | |
340 | 342 | | |
341 | 343 | | |
342 | | - | |
343 | | - | |
344 | 344 | | |
345 | 345 | | |
346 | 346 | | |
| |||
Lines changed: 22 additions & 3 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
92 | 92 | | |
93 | 93 | | |
94 | 94 | | |
95 | | - | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
96 | 106 | | |
97 | 107 | | |
98 | 108 | | |
| |||
300 | 310 | | |
301 | 311 | | |
302 | 312 | | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
303 | 316 | | |
304 | 317 | | |
305 | | - | |
| 318 | + | |
306 | 319 | | |
307 | 320 | | |
308 | 321 | | |
| |||
326 | 339 | | |
327 | 340 | | |
328 | 341 | | |
329 | | - | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
330 | 349 | | |
331 | 350 | | |
332 | 351 | | |
| |||
0 commit comments