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/errors.d.ts +13 -0
- package/esm/index.mjs +325 -104
- package/esm/package.json +2 -1
- package/generator.d.ts +13 -8
- package/index.d.ts +3 -1
- package/index.js +334 -104
- package/package.json +2 -1
- package/ssrf.d.ts +108 -0
- package/types.d.ts +16 -6
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
|
|
331
|
-
*
|
|
332
|
-
*
|
|
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
|
|
395
|
-
*
|
|
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;
|