mcp-from-openapi 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/ssrf.d.ts ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * SSRF protection for spec loading and external `$ref` resolution.
3
+ *
4
+ * The generator fetches two kinds of attacker-influenceable URLs:
5
+ * 1. the OpenAPI spec itself (`fromURL`), and
6
+ * 2. external `$ref` targets during dereferencing.
7
+ *
8
+ * A hostname-string denylist (the pre-2.5 approach) is bypassable, as reported
9
+ * in GHSA-65h7-9wrw-629c:
10
+ * - DNS names that resolve to internal IPs, e.g. `http://127.0.0.1.nip.io/`
11
+ * — the literal-string `127.0.0.1` patterns never match, yet the name
12
+ * resolves to loopback;
13
+ * - IPv4-mapped IPv6 forms (handled since 2.4 via {@link decodeIpv4MappedIpv6});
14
+ * - redirects from an allowed host to an internal one.
15
+ *
16
+ * This module validates the **resolved IP**, not just the hostname string:
17
+ * - parses/normalizes IPv4 (incl. numeric/hex/octal forms canonicalized by
18
+ * `new URL()`) and IPv4-mapped IPv6 before range checks;
19
+ * - blocks loopback / private (RFC 1918) / CGNAT (RFC 6598) / link-local /
20
+ * multicast / unspecified / reserved ranges and cloud-metadata endpoints;
21
+ * - resolves the hostname (Node, via `node:dns`) and rejects if **any**
22
+ * resolved address is internal — closing the DNS-name-to-internal bypass;
23
+ * - re-validates every redirect hop ({@link safeFetch}) instead of letting the
24
+ * HTTP client follow 3xx blindly.
25
+ *
26
+ * Node-aware: DNS resolution lazily imports `node:dns` and is a no-op on
27
+ * runtimes without it (Web/edge), where the literal-address checks still apply.
28
+ *
29
+ * Residual: this does DNS resolve-then-fetch without connection-level IP
30
+ * pinning, so a sub-second DNS-rebinding race (flip the record between the
31
+ * validating resolve and the client's connect-time resolve) is not fully
32
+ * eliminated. For fully-untrusted inputs, combine with an `allowedHosts`
33
+ * allow-list and network egress controls.
34
+ */
35
+ import type { RefResolutionOptions } from './types';
36
+ /** The subset of {@link RefResolutionOptions} relevant to address blocking. */
37
+ export interface ResolvedSsrfOptions {
38
+ allowedHosts: string[];
39
+ blockedHosts: string[];
40
+ allowInternalIPs: boolean;
41
+ }
42
+ /** Resolved DNS address shape (subset of Node's `dns.LookupAddress`). */
43
+ export interface ResolvedAddress {
44
+ address: string;
45
+ family: number;
46
+ }
47
+ /** Hostname → resolved addresses. Injectable for testing. */
48
+ export type SsrfHostLookup = (hostname: string) => Promise<ResolvedAddress[]>;
49
+ /**
50
+ * Non-IP hostnames that map to internal targets, so the IP-range checks alone
51
+ * would miss them when DNS resolution is unavailable. DNS resolution (when
52
+ * available) also catches these; this set is defense-in-depth.
53
+ */
54
+ export declare const BLOCKED_HOSTNAMES: ReadonlySet<string>;
55
+ /** Normalize a (possibly undefined) {@link RefResolutionOptions} to the SSRF subset. */
56
+ export declare function normalizeSsrfOptions(refResolution?: RefResolutionOptions): ResolvedSsrfOptions;
57
+ /**
58
+ * Decode an IPv4-mapped IPv6 host (`::ffff:169.254.169.254` or its hex form
59
+ * `::ffff:a9fe:a9fe`, optionally bracketed) to its embedded dotted-quad IPv4, or
60
+ * `null` if the host isn't IPv4-mapped. `new URL().hostname` normalizes
61
+ * `[::ffff:169.254.169.254]` to `[::ffff:a9fe:a9fe]`, which the plain
62
+ * dotted-quad range checks would otherwise miss.
63
+ */
64
+ export declare function decodeIpv4MappedIpv6(hostname: string): string | null;
65
+ /**
66
+ * Predicate: is `host` (an IP literal — dotted-quad IPv4, bracketed/zoned IPv6,
67
+ * or IPv4-mapped IPv6) in a blocked, non-public range? Returns `false` for
68
+ * non-IP-literal hostnames (use DNS resolution for those).
69
+ */
70
+ export declare function isBlockedAddress(host: string): boolean;
71
+ /**
72
+ * Synchronous host check used by the `$RefParser` `canRead` filter (which cannot
73
+ * be async). Blocks known internal hostnames, explicit `blockedHosts`, and
74
+ * literal internal IPs (incl. IPv4-mapped IPv6). DNS names that *resolve* to
75
+ * internal addresses are caught later, asynchronously, in {@link safeFetch}.
76
+ */
77
+ export declare function isBlockedHostname(hostname: string, ssrf: ResolvedSsrfOptions): boolean;
78
+ /** Default DNS resolver: lazily loads `node:dns`; rejects on non-Node runtimes. */
79
+ export declare const defaultLookup: SsrfHostLookup;
80
+ /**
81
+ * Validate that `url` is safe to fetch (spec URL or `$ref` target), throwing
82
+ * {@link SsrfError} if not. Enforces http/https, the `allowedHosts` allow-list,
83
+ * the internal-address denylist, and — for DNS names — resolves and rejects if
84
+ * any resolved address is internal.
85
+ */
86
+ export declare function assertUrlSafe(url: string, ssrf: ResolvedSsrfOptions, lookup?: SsrfHostLookup): Promise<void>;
87
+ /** Options for {@link safeFetch}. */
88
+ export interface SafeFetchOptions {
89
+ headers?: Record<string, string>;
90
+ timeoutMs?: number;
91
+ /** Follow 3xx redirects (re-validating each hop). @default true */
92
+ followRedirects?: boolean;
93
+ /** Max redirect hops before failing. @default 5 */
94
+ maxRedirects?: number;
95
+ ssrf: ResolvedSsrfOptions;
96
+ /** Injectable DNS resolver (tests). */
97
+ lookup?: SsrfHostLookup;
98
+ /** Injectable fetch implementation (tests / custom runtimes). */
99
+ fetchImpl?: typeof fetch;
100
+ }
101
+ /**
102
+ * SSRF-safe `fetch`: validates the initial URL and **every redirect hop** with
103
+ * {@link assertUrlSafe} before issuing the request, using manual redirect
104
+ * handling so a 3xx to an internal target can't be followed without
105
+ * re-validation. Returns the final {@link Response} (the caller checks
106
+ * `response.ok` / reads the body).
107
+ */
108
+ export declare function safeFetch(url: string, opts: SafeFetchOptions): Promise<Response>;
package/types.d.ts CHANGED
@@ -327,9 +327,15 @@ export interface ServerInfo {
327
327
  variables?: Record<string, ServerVariableObject>;
328
328
  }
329
329
  /**
330
- * Controls how external $ref pointers are resolved during dereferencing.
331
- * By default, only http/https protocols are allowed and internal/private
332
- * IP addresses are blocked to prevent SSRF attacks.
330
+ * Controls how external `$ref` pointers are resolved during dereferencing, and
331
+ * the host policy applied to the initial spec-URL fetch in `fromURL`.
332
+ *
333
+ * By default only http/https protocols are allowed and internal/private targets
334
+ * are blocked to prevent SSRF. As of 2.5.0 the guard validates the **resolved
335
+ * IP** (it resolves DNS and rejects hostnames that map to internal addresses —
336
+ * e.g. `127.0.0.1.nip.io`), normalizes IPv4-mapped IPv6, and re-validates every
337
+ * HTTP redirect hop. `allowedHosts` / `blockedHosts` / `allowInternalIPs` apply
338
+ * to both the spec URL and external `$ref`s.
333
339
  */
334
340
  export interface RefResolutionOptions {
335
341
  /**
@@ -386,13 +392,17 @@ export interface LoadOptions {
386
392
  */
387
393
  validate?: boolean;
388
394
  /**
389
- * Whether to follow HTTP redirects
395
+ * Whether to follow HTTP redirects when fetching the spec URL. Each redirect
396
+ * hop is re-validated against the SSRF guard before being followed (a 3xx to
397
+ * an internal target is refused), so following is safe by default.
390
398
  * @default true
391
399
  */
392
400
  followRedirects?: boolean;
393
401
  /**
394
- * Controls external $ref resolution security.
395
- * By default, file:// is blocked and internal IPs are blocked.
402
+ * Controls spec-loading security: external `$ref` resolution AND the host
403
+ * policy for the initial spec-URL fetch. By default `file://` is blocked,
404
+ * internal/private targets are blocked, and hostnames are DNS-resolved and
405
+ * re-checked against the internal-address ranges.
396
406
  * @see RefResolutionOptions
397
407
  */
398
408
  refResolution?: RefResolutionOptions;