@atproto-labs/fetch-node 0.1.9 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @atproto-labs/fetch-node
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#4289](https://github.com/bluesky-social/atproto/pull/4289) [`8ff5ec4ca`](https://github.com/bluesky-social/atproto/commit/8ff5ec4caa9a1f5c1e453a416ba2af22d1ee4f58) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove `isLocalHostname` export
8
+
9
+ ## 0.1.10
10
+
11
+ ### Patch Changes
12
+
13
+ - [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow forcing the use of keep-alive agent on older NodeJs version when unicast protection is active
14
+
15
+ - [#4108](https://github.com/bluesky-social/atproto/pull/4108) [`f9dc9aa4c`](https://github.com/bluesky-social/atproto/commit/f9dc9aa4c9eaf2f82d140fbf011a9015e7f1a00d) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Use keep-alive connection when unicast protection is enabled on NodeJS >= 20
16
+
3
17
  ## 0.1.9
4
18
 
5
19
  ### Patch Changes
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,sDAAmC;AAEnC,4CAAyB;AACzB,+CAA4B;AAC5B,4CAAyB"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,sDAAmC;AAEnC,4CAAyB;AACzB,+CAA4B;AAC5B,4CAAyB","sourcesContent":["export * from '@atproto-labs/fetch'\n\nexport * from './safe.js'\nexport * from './unicast.js'\nexport * from './util.js'\n"]}
package/dist/safe.d.ts CHANGED
@@ -1,30 +1,41 @@
1
- import { Fetch } from '@atproto-labs/fetch';
2
- export type SafeFetchWrapOptions = NonNullable<Parameters<typeof safeFetchWrap>[0]>;
1
+ import { UnicastFetchWrapOptions } from './unicast.js';
2
+ export type SafeFetchWrapOptions<C> = UnicastFetchWrapOptions<C> & {
3
+ responseMaxSize?: number;
4
+ ssrfProtection?: boolean;
5
+ allowCustomPort?: boolean;
6
+ allowData?: boolean;
7
+ allowHttp?: boolean;
8
+ allowIpHost?: boolean;
9
+ allowPrivateIps?: boolean;
10
+ timeout?: number;
11
+ forbiddenDomainNames?: Iterable<string>;
12
+ /**
13
+ * When `false`, a {@link RequestInit['redirect']} value must be explicitly
14
+ * provided as second argument to the returned function or requests will fail.
15
+ *
16
+ * @default false
17
+ */
18
+ allowImplicitRedirect?: boolean;
19
+ };
3
20
  /**
4
21
  * Wrap a fetch function with safety checks so that it can be safely used
5
22
  * with user provided input (URL).
6
23
  *
7
24
  * @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html}
8
- */
9
- export declare function safeFetchWrap({ fetch, responseMaxSize, // 512kB
10
- ssrfProtection, allowCustomPort, allowData, allowHttp, allowIpHost, allowPrivateIps, timeout, forbiddenDomainNames,
11
- /**
12
- * When `false`, a {@link RequestInit['redirect']} value must be explicitly
13
- * provided or the request will fail.
14
25
  *
15
- * @default false
26
+ * @note When {@link SafeFetchWrapOptions.allowImplicitRedirect} is `false`
27
+ * (default), then the returned function **must** be called setting the second
28
+ * argument's `redirect` property to one of the allowed values. Otherwise, if
29
+ * the returned fetch function is called with a `Request` object (and no
30
+ * explicit `redirect` init object), then the verification code will not be able
31
+ * to determine if the `redirect` property was explicitly set or based on the
32
+ * default value (`follow`), causing it to preventively block the request (throw
33
+ * an error). For this reason, unless you set
34
+ * {@link SafeFetchWrapOptions.allowImplicitRedirect} to `true`, you should
35
+ * **not** wrap the returned function into another function that creates a
36
+ * {@link Request} object before passing it to the function (as a e.g. a logging
37
+ * function would).
16
38
  */
17
- allowImplicitRedirect, }?: {
18
- fetch?: Fetch | undefined;
19
- responseMaxSize?: number | undefined;
20
- ssrfProtection?: boolean | undefined;
21
- allowCustomPort?: boolean | undefined;
22
- allowData?: boolean | undefined;
23
- allowHttp?: boolean | undefined;
24
- allowIpHost?: boolean | undefined;
25
- allowPrivateIps?: boolean | undefined;
26
- timeout?: number | undefined;
27
- forbiddenDomainNames?: Iterable<string> | undefined;
28
- allowImplicitRedirect?: boolean | undefined;
29
- }): (input: string | URL | Request, init?: RequestInit | undefined) => Promise<Response>;
39
+ export declare function safeFetchWrap<C>({ fetch, dangerouslyForceKeepAliveAgent, responseMaxSize, // 512kB
40
+ ssrfProtection, allowCustomPort, allowData, allowHttp, allowIpHost, allowPrivateIps, timeout, forbiddenDomainNames, allowImplicitRedirect, }?: SafeFetchWrapOptions<C>): (input: string | URL | Request, init?: RequestInit | undefined) => Promise<Response>;
30
41
  //# sourceMappingURL=safe.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"safe.d.ts","sourceRoot":"","sources":["../src/safe.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,EAQN,MAAM,qBAAqB,CAAA;AAI5B,MAAM,MAAM,oBAAoB,GAAG,WAAW,CAC5C,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC,CAAC,CAAC,CACpC,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAC5B,KAAiC,EACjC,eAA4B,EAAE,QAAQ;AACtC,cAAqB,EACrB,eAAiC,EACjC,SAAiB,EACjB,SAA2B,EAC3B,WAAkB,EAClB,eAAiC,EACjC,OAAc,EACd,oBAAyE;AACzE;;;;;GAKG;AACH,qBAA6B,GAC9B;;;;;;;;;;;;CAAK,wFAoDL"}
1
+ {"version":3,"file":"safe.d.ts","sourceRoot":"","sources":["../src/safe.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,uBAAuB,EAAoB,MAAM,cAAc,CAAA;AAExE,MAAM,MAAM,oBAAoB,CAAC,CAAC,IAAI,uBAAuB,CAAC,CAAC,CAAC,GAAG;IACjE,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,WAAW,CAAC,EAAE,OAAO,CAAA;IACrB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,oBAAoB,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAA;IACvC;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,OAAO,CAAA;CAChC,CAAA;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,EAC/B,KAAoC,EACpC,8BAAsC,EACtC,eAA4B,EAAE,QAAQ;AACtC,cAAqB,EACrB,eAAiC,EACjC,SAAiB,EACjB,SAA2B,EAC3B,WAAkB,EAClB,eAAiC,EACjC,OAAc,EACd,oBAAyE,EACzE,qBAA6B,GAC9B,GAAE,oBAAoB,CAAC,CAAC,CAAM,wFAsD9B"}
package/dist/safe.js CHANGED
@@ -9,16 +9,22 @@ const unicast_js_1 = require("./unicast.js");
9
9
  * with user provided input (URL).
10
10
  *
11
11
  * @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html}
12
- */
13
- function safeFetchWrap({ fetch = globalThis.fetch, responseMaxSize = 512 * 1024, // 512kB
14
- ssrfProtection = true, allowCustomPort = !ssrfProtection, allowData = false, allowHttp = !ssrfProtection, allowIpHost = true, allowPrivateIps = !ssrfProtection, timeout = 10e3, forbiddenDomainNames = fetch_1.DEFAULT_FORBIDDEN_DOMAIN_NAMES,
15
- /**
16
- * When `false`, a {@link RequestInit['redirect']} value must be explicitly
17
- * provided or the request will fail.
18
12
  *
19
- * @default false
13
+ * @note When {@link SafeFetchWrapOptions.allowImplicitRedirect} is `false`
14
+ * (default), then the returned function **must** be called setting the second
15
+ * argument's `redirect` property to one of the allowed values. Otherwise, if
16
+ * the returned fetch function is called with a `Request` object (and no
17
+ * explicit `redirect` init object), then the verification code will not be able
18
+ * to determine if the `redirect` property was explicitly set or based on the
19
+ * default value (`follow`), causing it to preventively block the request (throw
20
+ * an error). For this reason, unless you set
21
+ * {@link SafeFetchWrapOptions.allowImplicitRedirect} to `true`, you should
22
+ * **not** wrap the returned function into another function that creates a
23
+ * {@link Request} object before passing it to the function (as a e.g. a logging
24
+ * function would).
20
25
  */
21
- allowImplicitRedirect = false, } = {}) {
26
+ function safeFetchWrap({ fetch = globalThis.fetch, dangerouslyForceKeepAliveAgent = false, responseMaxSize = 512 * 1024, // 512kB
27
+ ssrfProtection = true, allowCustomPort = !ssrfProtection, allowData = false, allowHttp = !ssrfProtection, allowIpHost = true, allowPrivateIps = !ssrfProtection, timeout = 10e3, forbiddenDomainNames = fetch_1.DEFAULT_FORBIDDEN_DOMAIN_NAMES, allowImplicitRedirect = false, } = {}) {
22
28
  return (0, pipe_1.pipe)(
23
29
  /**
24
30
  * Require explicit {@link RequestInit['redirect']} mode
@@ -55,7 +61,9 @@ allowImplicitRedirect = false, } = {}) {
55
61
  * input, we need to make sure that the request is not vulnerable to SSRF
56
62
  * attacks.
57
63
  */
58
- allowPrivateIps ? fetch : (0, unicast_js_1.unicastFetchWrap)({ fetch })),
64
+ allowPrivateIps
65
+ ? fetch
66
+ : (0, unicast_js_1.unicastFetchWrap)({ fetch, dangerouslyForceKeepAliveAgent })),
59
67
  /**
60
68
  * Since we will be fetching user owned data, we need to make sure that an
61
69
  * attacker cannot force us to download a large amounts of data.
package/dist/safe.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"safe.js","sourceRoot":"","sources":["../src/safe.ts"],"names":[],"mappings":";;AAwBA,sCAsEC;AA9FD,+CAU4B;AAC5B,6CAAyC;AACzC,6CAA+C;AAM/C;;;;;GAKG;AACH,SAAgB,aAAa,CAAC,EAC5B,KAAK,GAAG,UAAU,CAAC,KAAc,EACjC,eAAe,GAAG,GAAG,GAAG,IAAI,EAAE,QAAQ;AACtC,cAAc,GAAG,IAAI,EACrB,eAAe,GAAG,CAAC,cAAc,EACjC,SAAS,GAAG,KAAK,EACjB,SAAS,GAAG,CAAC,cAAc,EAC3B,WAAW,GAAG,IAAI,EAClB,eAAe,GAAG,CAAC,cAAc,EACjC,OAAO,GAAG,IAAI,EACd,oBAAoB,GAAG,sCAAkD;AACzE;;;;;GAKG;AACH,qBAAqB,GAAG,KAAK,MAC3B,EAAE;IACJ,OAAO,IAAA,WAAI;IACT;;OAEG;IACH,qBAAqB,CAAC,CAAC,CAAC,iBAAS,CAAC,CAAC,CAAC,IAAA,6CAAqC,GAAE;IAE3E;;OAEG;IACH,WAAW,CAAC,CAAC,CAAC,iBAAS,CAAC,CAAC,CAAC,IAAA,kCAA0B,GAAE;IAEtD;;OAEG;IACH,IAAA,qCAA6B,EAAC;QAC5B,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,SAAS;QAClB,OAAO,EAAE,KAAK;QACd,OAAO,EAAE,SAAS,IAAI,EAAE,eAAe,EAAE;QACzC,QAAQ,EAAE,EAAE,eAAe,EAAE;KAC9B,CAAC;IAEF;;;;;OAKG;IACH,IAAA,2CAAmC,EAAC,oBAAoB,CAAC;IAEzD;;;OAGG;IACH,IAAA,kBAAU,EACR,OAAO;IAEP;;;;OAIG;IACH,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAA,6BAAgB,EAAC,EAAE,KAAK,EAAE,CAAC,CACtD;IAED;;;OAGG;IACH,IAAA,6BAAqB,EAAC,eAAe,CAAC,CACd,CAAA;AAC5B,CAAC"}
1
+ {"version":3,"file":"safe.js","sourceRoot":"","sources":["../src/safe.ts"],"names":[],"mappings":";;AAoDA,sCAmEC;AAvHD,+CAU4B;AAC5B,6CAAyC;AACzC,6CAAwE;AAqBxE;;;;;;;;;;;;;;;;;;GAkBG;AACH,SAAgB,aAAa,CAAI,EAC/B,KAAK,GAAG,UAAU,CAAC,KAAiB,EACpC,8BAA8B,GAAG,KAAK,EACtC,eAAe,GAAG,GAAG,GAAG,IAAI,EAAE,QAAQ;AACtC,cAAc,GAAG,IAAI,EACrB,eAAe,GAAG,CAAC,cAAc,EACjC,SAAS,GAAG,KAAK,EACjB,SAAS,GAAG,CAAC,cAAc,EAC3B,WAAW,GAAG,IAAI,EAClB,eAAe,GAAG,CAAC,cAAc,EACjC,OAAO,GAAG,IAAI,EACd,oBAAoB,GAAG,sCAAkD,EACzE,qBAAqB,GAAG,KAAK,MACF,EAAE;IAC7B,OAAO,IAAA,WAAI;IACT;;OAEG;IACH,qBAAqB,CAAC,CAAC,CAAC,iBAAS,CAAC,CAAC,CAAC,IAAA,6CAAqC,GAAE;IAE3E;;OAEG;IACH,WAAW,CAAC,CAAC,CAAC,iBAAS,CAAC,CAAC,CAAC,IAAA,kCAA0B,GAAE;IAEtD;;OAEG;IACH,IAAA,qCAA6B,EAAC;QAC5B,QAAQ,EAAE,KAAK;QACf,OAAO,EAAE,SAAS;QAClB,OAAO,EAAE,KAAK;QACd,OAAO,EAAE,SAAS,IAAI,EAAE,eAAe,EAAE;QACzC,QAAQ,EAAE,EAAE,eAAe,EAAE;KAC9B,CAAC;IAEF;;;;;OAKG;IACH,IAAA,2CAAmC,EAAC,oBAAoB,CAAC;IAEzD;;;OAGG;IACH,IAAA,kBAAU,EACR,OAAO;IAEP;;;;OAIG;IACH,eAAe;QACb,CAAC,CAAC,KAAK;QACP,CAAC,CAAC,IAAA,6BAAgB,EAAC,EAAE,KAAK,EAAE,8BAA8B,EAAE,CAAC,CAChE;IAED;;;OAGG;IACH,IAAA,6BAAqB,EAAC,eAAe,CAAC,CACd,CAAA;AAC5B,CAAC","sourcesContent":["import {\n DEFAULT_FORBIDDEN_DOMAIN_NAMES,\n Fetch,\n asRequest,\n explicitRedirectCheckRequestTransform,\n fetchMaxSizeProcessor,\n forbiddenDomainNameRequestTransform,\n protocolCheckRequestTransform,\n requireHostHeaderTransform,\n timedFetch,\n} from '@atproto-labs/fetch'\nimport { pipe } from '@atproto-labs/pipe'\nimport { UnicastFetchWrapOptions, unicastFetchWrap } from './unicast.js'\n\nexport type SafeFetchWrapOptions<C> = UnicastFetchWrapOptions<C> & {\n responseMaxSize?: number\n ssrfProtection?: boolean\n allowCustomPort?: boolean\n allowData?: boolean\n allowHttp?: boolean\n allowIpHost?: boolean\n allowPrivateIps?: boolean\n timeout?: number\n forbiddenDomainNames?: Iterable<string>\n /**\n * When `false`, a {@link RequestInit['redirect']} value must be explicitly\n * provided as second argument to the returned function or requests will fail.\n *\n * @default false\n */\n allowImplicitRedirect?: boolean\n}\n\n/**\n * Wrap a fetch function with safety checks so that it can be safely used\n * with user provided input (URL).\n *\n * @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html}\n *\n * @note When {@link SafeFetchWrapOptions.allowImplicitRedirect} is `false`\n * (default), then the returned function **must** be called setting the second\n * argument's `redirect` property to one of the allowed values. Otherwise, if\n * the returned fetch function is called with a `Request` object (and no\n * explicit `redirect` init object), then the verification code will not be able\n * to determine if the `redirect` property was explicitly set or based on the\n * default value (`follow`), causing it to preventively block the request (throw\n * an error). For this reason, unless you set\n * {@link SafeFetchWrapOptions.allowImplicitRedirect} to `true`, you should\n * **not** wrap the returned function into another function that creates a\n * {@link Request} object before passing it to the function (as a e.g. a logging\n * function would).\n */\nexport function safeFetchWrap<C>({\n fetch = globalThis.fetch as Fetch<C>,\n dangerouslyForceKeepAliveAgent = false,\n responseMaxSize = 512 * 1024, // 512kB\n ssrfProtection = true,\n allowCustomPort = !ssrfProtection,\n allowData = false,\n allowHttp = !ssrfProtection,\n allowIpHost = true,\n allowPrivateIps = !ssrfProtection,\n timeout = 10e3,\n forbiddenDomainNames = DEFAULT_FORBIDDEN_DOMAIN_NAMES as Iterable<string>,\n allowImplicitRedirect = false,\n}: SafeFetchWrapOptions<C> = {}) {\n return pipe(\n /**\n * Require explicit {@link RequestInit['redirect']} mode\n */\n allowImplicitRedirect ? asRequest : explicitRedirectCheckRequestTransform(),\n\n /**\n * Only requests that will be issued with a \"Host\" header are allowed.\n */\n allowIpHost ? asRequest : requireHostHeaderTransform(),\n\n /**\n * Prevent using http:, file: or data: protocols.\n */\n protocolCheckRequestTransform({\n 'about:': false,\n 'data:': allowData,\n 'file:': false,\n 'http:': allowHttp && { allowCustomPort },\n 'https:': { allowCustomPort },\n }),\n\n /**\n * Disallow fetching from domains we know are not atproto/OIDC client\n * implementation. Note that other domains can be blocked by providing a\n * custom fetch function combined with another\n * forbiddenDomainNameRequestTransform.\n */\n forbiddenDomainNameRequestTransform(forbiddenDomainNames),\n\n /**\n * Since we will be fetching from the network based on user provided\n * input, let's mitigate resource exhaustion attacks by setting a timeout.\n */\n timedFetch(\n timeout,\n\n /**\n * Since we will be fetching from the network based on user provided\n * input, we need to make sure that the request is not vulnerable to SSRF\n * attacks.\n */\n allowPrivateIps\n ? fetch\n : unicastFetchWrap({ fetch, dangerouslyForceKeepAliveAgent }),\n ),\n\n /**\n * Since we will be fetching user owned data, we need to make sure that an\n * attacker cannot force us to download a large amounts of data.\n */\n fetchMaxSizeProcessor(responseMaxSize),\n ) satisfies Fetch<unknown>\n}\n"]}
package/dist/unicast.d.ts CHANGED
@@ -1,18 +1,40 @@
1
1
  import dns from 'node:dns';
2
2
  import { LookupFunction } from 'node:net';
3
3
  import { Fetch, FetchContext } from '@atproto-labs/fetch';
4
- export type SsrfFetchWrapOptions<C = FetchContext> = {
4
+ export type UnicastFetchWrapOptions<C = FetchContext> = {
5
5
  fetch?: Fetch<C>;
6
+ /**
7
+ * ## ‼️ important security feature use with care
8
+ *
9
+ * On older NodeJS version, the `dispatcher` init option is ignored when
10
+ * creating a new Request instance. It can only be passed through the fetch
11
+ * function directly.
12
+ *
13
+ * Since this is a security feature, we need to ensure that the unicastLookup
14
+ * function is called to resolve the hostname to a unicast IP address.
15
+ *
16
+ * However, in the case a custom "fetch" function is passed here (fetch !==
17
+ * globalThis.fetch), we have no guarantee that the dispatcher will be used to
18
+ * make the request. Because of this, in such a case, we will use a one-time
19
+ * use dispatcher that checks that the provided fetch function indeed made use
20
+ * of the "unicastLookup" when a custom dispatch init function is used.
21
+ *
22
+ * Sadly, this means that we cannot use "keepAlive" connections, as the method
23
+ * used to ensure that "unicastLookup" gets called requires to create a new
24
+ * dispatcher for each request.
25
+ *
26
+ * If you can guarantee that the provided fetch function will make use of the
27
+ * "dispatcher" init option, you can set this flag to true, which will enable
28
+ * the use of a single agent (with keep-alive) for all requests.
29
+ *
30
+ * @default false
31
+ * @note This option has no effect on Node.js versions >= 20
32
+ */
33
+ dangerouslyForceKeepAliveAgent?: boolean;
6
34
  };
7
35
  /**
8
36
  * @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/}
9
37
  */
10
- export declare function unicastFetchWrap<C = FetchContext>({ fetch, }: SsrfFetchWrapOptions<C>): Fetch<C>;
38
+ export declare function unicastFetchWrap<C = FetchContext>({ fetch, dangerouslyForceKeepAliveAgent, }: UnicastFetchWrapOptions<C>): Fetch<C>;
11
39
  export declare function unicastLookup(hostname: string, options: dns.LookupOptions, callback: Parameters<LookupFunction>[2]): void;
12
- /**
13
- * @param hostname - a syntactically valid hostname
14
- * @returns whether the hostname is a name typically used for on locale area networks.
15
- * @note **DO NOT** use for security reasons. Only as heuristic.
16
- */
17
- export declare function isLocalHostname(hostname: string): boolean;
18
40
  //# sourceMappingURL=unicast.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"unicast.d.ts","sourceRoot":"","sources":["../src/unicast.ts"],"names":[],"mappings":"AAAA,OAAO,GAAsB,MAAM,UAAU,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAGzC,OAAO,EACL,KAAK,EACL,YAAY,EAIb,MAAM,qBAAqB,CAAA;AAK5B,MAAM,MAAM,oBAAoB,CAAC,CAAC,GAAG,YAAY,IAAI;IACnD,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAA;CACjB,CAAA;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,GAAG,YAAY,EAAE,EACjD,KAAwB,GACzB,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CA0IpC;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,GAAG,CAAC,aAAa,EAC1B,QAAQ,EAAE,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,QA0BxC;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAYzD"}
1
+ {"version":3,"file":"unicast.d.ts","sourceRoot":"","sources":["../src/unicast.ts"],"names":[],"mappings":"AAAA,OAAO,GAAsB,MAAM,UAAU,CAAA;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAGzC,OAAO,EACL,KAAK,EACL,YAAY,EAIb,MAAM,qBAAqB,CAAA;AAK5B,MAAM,MAAM,uBAAuB,CAAC,CAAC,GAAG,YAAY,IAAI;IACtD,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAA;IAEhB;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,8BAA8B,CAAC,EAAE,OAAO,CAAA;CACzC,CAAA;AAMD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,GAAG,YAAY,EAAE,EACjD,KAAwB,EACxB,8BAAsC,GACvC,EAAE,uBAAuB,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAuIvC;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,GAAG,CAAC,aAAa,EAC1B,QAAQ,EAAE,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,QA0BxC"}
package/dist/unicast.js CHANGED
@@ -5,29 +5,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.unicastFetchWrap = unicastFetchWrap;
7
7
  exports.unicastLookup = unicastLookup;
8
- exports.isLocalHostname = isLocalHostname;
9
8
  const node_dns_1 = __importDefault(require("node:dns"));
10
9
  const ipaddr_js_1 = __importDefault(require("ipaddr.js"));
11
10
  const undici_1 = require("undici");
12
11
  const fetch_1 = require("@atproto-labs/fetch");
13
12
  const util_js_1 = require("./util.js");
14
13
  const { IPv4, IPv6 } = ipaddr_js_1.default;
14
+ // @TODO support other runtimes ?
15
+ const SUPPORTS_REQUEST_INIT_DISPATCHER = Number(process.versions.node.split('.')[0]) >= 20;
15
16
  /**
16
17
  * @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/}
17
18
  */
18
- function unicastFetchWrap({ fetch = globalThis.fetch, }) {
19
- // In order to enforce the SSRF protection, we need to use a custom dispatcher
20
- // that uses "unicastLookup" to resolve the hostname to a unicast IP address.
21
- // In case a custom "fetch" function is passed here, we have no assurance that
22
- // the dispatcher will be used to make the request. Because of this, in case a
23
- // custom fetch method is passed, we will use a on-time use dispatcher that
24
- // ensures that "unicastLookup" gets called to resolve the hostname to an IP
25
- // address and ensure that it is a unicast address.
26
- // Sadly, this means that we cannot use "keepAlive" connections, as the method
27
- // used to ensure that "unicastLookup" gets called requires to create a new
28
- // dispatcher for each request.
29
- // @TODO: find a way to use a re-usable dispatcher with a custom fetch method.
30
- if (fetch === globalThis.fetch) {
19
+ function unicastFetchWrap({ fetch = globalThis.fetch, dangerouslyForceKeepAliveAgent = false, }) {
20
+ if (SUPPORTS_REQUEST_INIT_DISPATCHER ||
21
+ dangerouslyForceKeepAliveAgent ||
22
+ fetch === globalThis.fetch) {
31
23
  const dispatcher = new undici_1.Agent({
32
24
  connect: { lookup: unicastLookup },
33
25
  });
@@ -39,8 +31,15 @@ function unicastFetchWrap({ fetch = globalThis.fetch, }) {
39
31
  if (url.hostname && (0, util_js_1.isUnicastIp)(url.hostname) === false) {
40
32
  throw new fetch_1.FetchRequestError((0, fetch_1.asRequest)(input, init), 400, 'Hostname is a non-unicast address');
41
33
  }
42
- // @ts-expect-error non-standard option
43
- return fetch.call(this, input, { ...init, dispatcher });
34
+ if (SUPPORTS_REQUEST_INIT_DISPATCHER) {
35
+ // @ts-expect-error non-standard option
36
+ const request = new Request(input, { ...init, dispatcher });
37
+ return fetch.call(this, request);
38
+ }
39
+ else {
40
+ // @ts-expect-error non-standard option
41
+ return fetch.call(this, input, { ...init, dispatcher });
42
+ }
44
43
  };
45
44
  }
46
45
  else {
@@ -61,13 +60,15 @@ function unicastFetchWrap({ fetch = globalThis.fetch, }) {
61
60
  throw new fetch_1.FetchRequestError((0, fetch_1.asRequest)(input, init), 400, 'Hostname is a non-unicast address');
62
61
  }
63
62
  case undefined: {
64
- // hostname is a domain name, using the dispatcher defined above
65
- // will result in the DNS lookup being performed, ensuring that the
66
- // hostname resolves to a unicast address.
63
+ // hostname is a domain name, let's create a new dispatcher that
64
+ // will 1) use the unicastLookup function to resolve the hostname
65
+ // and 2) allow us to check that the lookup function was indeed
66
+ // called.
67
67
  let didLookup = false;
68
68
  const dispatcher = new undici_1.Client(url.origin, {
69
- // Do *not* enable H2 here, as it will cause an error (the client
70
- // will terminate the connection before the response is consumed).
69
+ // Do *not* enable H2 here, as it will cause an error (the
70
+ // client will terminate the connection before the response is
71
+ // consumed).
71
72
  // https://github.com/nodejs/undici/issues/3671
72
73
  connect: {
73
74
  keepAlive: false, // Client will be used once
@@ -1 +1 @@
1
- {"version":3,"file":"unicast.js","sourceRoot":"","sources":["../src/unicast.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"unicast.js","sourceRoot":"","sources":["../src/unicast.ts"],"names":[],"mappings":"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","sourcesContent":["import dns, { LookupAddress } from 'node:dns'\nimport { LookupFunction } from 'node:net'\nimport ipaddr from 'ipaddr.js'\nimport { Agent, Client } from 'undici'\nimport {\n Fetch,\n FetchContext,\n FetchRequestError,\n asRequest,\n extractUrl,\n} from '@atproto-labs/fetch'\nimport { isUnicastIp } from './util.js'\n\nconst { IPv4, IPv6 } = ipaddr\n\nexport type UnicastFetchWrapOptions<C = FetchContext> = {\n fetch?: Fetch<C>\n\n /**\n * ## ‼️ important security feature use with care\n *\n * On older NodeJS version, the `dispatcher` init option is ignored when\n * creating a new Request instance. It can only be passed through the fetch\n * function directly.\n *\n * Since this is a security feature, we need to ensure that the unicastLookup\n * function is called to resolve the hostname to a unicast IP address.\n *\n * However, in the case a custom \"fetch\" function is passed here (fetch !==\n * globalThis.fetch), we have no guarantee that the dispatcher will be used to\n * make the request. Because of this, in such a case, we will use a one-time\n * use dispatcher that checks that the provided fetch function indeed made use\n * of the \"unicastLookup\" when a custom dispatch init function is used.\n *\n * Sadly, this means that we cannot use \"keepAlive\" connections, as the method\n * used to ensure that \"unicastLookup\" gets called requires to create a new\n * dispatcher for each request.\n *\n * If you can guarantee that the provided fetch function will make use of the\n * \"dispatcher\" init option, you can set this flag to true, which will enable\n * the use of a single agent (with keep-alive) for all requests.\n *\n * @default false\n * @note This option has no effect on Node.js versions >= 20\n */\n dangerouslyForceKeepAliveAgent?: boolean\n}\n\n// @TODO support other runtimes ?\nconst SUPPORTS_REQUEST_INIT_DISPATCHER =\n Number(process.versions.node.split('.')[0]) >= 20\n\n/**\n * @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/}\n */\nexport function unicastFetchWrap<C = FetchContext>({\n fetch = globalThis.fetch,\n dangerouslyForceKeepAliveAgent = false,\n}: UnicastFetchWrapOptions<C>): Fetch<C> {\n if (\n SUPPORTS_REQUEST_INIT_DISPATCHER ||\n dangerouslyForceKeepAliveAgent ||\n fetch === globalThis.fetch\n ) {\n const dispatcher = new Agent({\n connect: { lookup: unicastLookup },\n })\n\n return async function (input, init): Promise<Response> {\n if (init?.dispatcher) {\n throw new FetchRequestError(\n asRequest(input, init),\n 500,\n 'SSRF protection cannot be used with a custom request dispatcher',\n )\n }\n\n const url = extractUrl(input)\n\n if (url.hostname && isUnicastIp(url.hostname) === false) {\n throw new FetchRequestError(\n asRequest(input, init),\n 400,\n 'Hostname is a non-unicast address',\n )\n }\n\n if (SUPPORTS_REQUEST_INIT_DISPATCHER) {\n // @ts-expect-error non-standard option\n const request = new Request(input, { ...init, dispatcher })\n return fetch.call(this, request)\n } else {\n // @ts-expect-error non-standard option\n return fetch.call(this, input, { ...init, dispatcher })\n }\n }\n } else {\n return async function (input, init): Promise<Response> {\n if (init?.dispatcher) {\n throw new FetchRequestError(\n asRequest(input, init),\n 500,\n 'SSRF protection cannot be used with a custom request dispatcher',\n )\n }\n\n const url = extractUrl(input)\n\n if (!url.hostname) {\n return fetch.call(this, input, init)\n }\n\n switch (isUnicastIp(url.hostname)) {\n case true: {\n // hostname is a unicast address, safe to proceed.\n return fetch.call(this, input, init)\n }\n\n case false: {\n throw new FetchRequestError(\n asRequest(input, init),\n 400,\n 'Hostname is a non-unicast address',\n )\n }\n\n case undefined: {\n // hostname is a domain name, let's create a new dispatcher that\n // will 1) use the unicastLookup function to resolve the hostname\n // and 2) allow us to check that the lookup function was indeed\n // called.\n\n let didLookup = false\n const dispatcher = new Client(url.origin, {\n // Do *not* enable H2 here, as it will cause an error (the\n // client will terminate the connection before the response is\n // consumed).\n // https://github.com/nodejs/undici/issues/3671\n connect: {\n keepAlive: false, // Client will be used once\n lookup(...args) {\n didLookup = true\n unicastLookup(...args)\n },\n },\n })\n\n try {\n const headers = new Headers(init?.headers)\n headers.set('connection', 'close') // Proactively close the connection\n\n const response = await fetch.call(this, input, {\n ...init,\n headers,\n // @ts-expect-error non-standard option\n dispatcher,\n })\n\n if (!didLookup) {\n // We need to ensure that the body is discarded. We can either\n // consume the whole body (for await loop) in order to keep the\n // socket alive, or cancel the request. Since we sent \"connection:\n // close\", there is no point in consuming the whole response\n // (which would cause un-necessary bandwidth).\n //\n // https://undici.nodejs.org/#/?id=garbage-collection\n await response.body?.cancel()\n\n // If you encounter this error, either upgrade to Node.js >=21 or\n // make sure that the dispatcher passed through the requestInit\n // object ends up being used to make the request.\n\n // eslint-disable-next-line no-unsafe-finally\n throw new FetchRequestError(\n asRequest(input, init),\n 500,\n 'Unable to enforce SSRF protection',\n )\n }\n\n return response\n } finally {\n // Free resources (we cannot await here since the response was not\n // consumed yet).\n void dispatcher.close().catch((err) => {\n // No biggie, but let's still log it\n console.warn('Failed to close dispatcher', err)\n })\n }\n }\n }\n }\n }\n}\n\nexport function unicastLookup(\n hostname: string,\n options: dns.LookupOptions,\n callback: Parameters<LookupFunction>[2],\n) {\n if (isLocalHostname(hostname)) {\n callback(new Error('Hostname is not a public domain'), [])\n return\n }\n\n dns.lookup(hostname, options, (err, address, family) => {\n if (err) {\n callback(err, address, family)\n } else {\n const ips = Array.isArray(address)\n ? address.map(parseLookupAddress)\n : [parseLookupAddress({ address, family })]\n\n if (ips.some(isNotUnicast)) {\n callback(\n new Error('Hostname resolved to non-unicast address'),\n address,\n family,\n )\n } else {\n callback(null, address, family)\n }\n }\n })\n}\n\n/**\n * @param hostname - a syntactically valid hostname\n * @returns whether the hostname is a name typically used for on locale area networks.\n * @note **DO NOT** use for security reasons. Only as heuristic.\n */\nfunction isLocalHostname(hostname: string): boolean {\n const parts = hostname.split('.')\n if (parts.length < 2) return true\n\n const tld = parts.at(-1)!.toLowerCase()\n return (\n tld === 'test' ||\n tld === 'local' ||\n tld === 'localhost' ||\n tld === 'invalid' ||\n tld === 'example'\n )\n}\n\nfunction isNotUnicast(ip: ipaddr.IPv4 | ipaddr.IPv6): boolean {\n return ip.range() !== 'unicast'\n}\n\nfunction parseLookupAddress({\n address,\n family,\n}: LookupAddress): ipaddr.IPv4 | ipaddr.IPv6 {\n const ip = family === 4 ? IPv4.parse(address) : IPv6.parse(address)\n\n if (ip instanceof IPv6 && ip.isIPv4MappedAddress()) {\n return ip.toIPv4Address()\n } else {\n return ip\n }\n}\n"]}
package/dist/util.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":";;;;;AAkBA,kCAGC;AArBD,0DAA8B;AAE9B,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,mBAAM,CAAA;AAE7B,SAAS,eAAe,CACtB,QAAgB;IAEhB,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IAC7B,CAAC;IAED,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACvD,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1C,CAAC;IAED,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,SAAgB,WAAW,CAAC,QAAgB;IAC1C,MAAM,EAAE,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAA;IACpC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAA;AAClD,CAAC"}
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":";;;;;AAkBA,kCAGC;AArBD,0DAA8B;AAE9B,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,mBAAM,CAAA;AAE7B,SAAS,eAAe,CACtB,QAAgB;IAEhB,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC1B,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;IAC7B,CAAC;IAED,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QACvD,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAA;IAC1C,CAAC;IAED,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,SAAgB,WAAW,CAAC,QAAgB;IAC1C,MAAM,EAAE,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAA;IACpC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAA;AAClD,CAAC","sourcesContent":["import ipaddr from 'ipaddr.js'\n\nconst { IPv4, IPv6 } = ipaddr\n\nfunction parseIpHostname(\n hostname: string,\n): ipaddr.IPv4 | ipaddr.IPv6 | undefined {\n if (IPv4.isIPv4(hostname)) {\n return IPv4.parse(hostname)\n }\n\n if (hostname.startsWith('[') && hostname.endsWith(']')) {\n return IPv6.parse(hostname.slice(1, -1))\n }\n\n return undefined\n}\n\nexport function isUnicastIp(hostname: string): boolean | undefined {\n const ip = parseIpHostname(hostname)\n return ip ? ip.range() === 'unicast' : undefined\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto-labs/fetch-node",
3
- "version": "0.1.9",
3
+ "version": "0.2.0",
4
4
  "license": "MIT",
5
5
  "description": "SSRF protection for fetch() in Node.js",
6
6
  "keywords": [
package/src/safe.ts CHANGED
@@ -10,20 +10,49 @@ import {
10
10
  timedFetch,
11
11
  } from '@atproto-labs/fetch'
12
12
  import { pipe } from '@atproto-labs/pipe'
13
- import { unicastFetchWrap } from './unicast.js'
13
+ import { UnicastFetchWrapOptions, unicastFetchWrap } from './unicast.js'
14
14
 
15
- export type SafeFetchWrapOptions = NonNullable<
16
- Parameters<typeof safeFetchWrap>[0]
17
- >
15
+ export type SafeFetchWrapOptions<C> = UnicastFetchWrapOptions<C> & {
16
+ responseMaxSize?: number
17
+ ssrfProtection?: boolean
18
+ allowCustomPort?: boolean
19
+ allowData?: boolean
20
+ allowHttp?: boolean
21
+ allowIpHost?: boolean
22
+ allowPrivateIps?: boolean
23
+ timeout?: number
24
+ forbiddenDomainNames?: Iterable<string>
25
+ /**
26
+ * When `false`, a {@link RequestInit['redirect']} value must be explicitly
27
+ * provided as second argument to the returned function or requests will fail.
28
+ *
29
+ * @default false
30
+ */
31
+ allowImplicitRedirect?: boolean
32
+ }
18
33
 
19
34
  /**
20
35
  * Wrap a fetch function with safety checks so that it can be safely used
21
36
  * with user provided input (URL).
22
37
  *
23
38
  * @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html}
39
+ *
40
+ * @note When {@link SafeFetchWrapOptions.allowImplicitRedirect} is `false`
41
+ * (default), then the returned function **must** be called setting the second
42
+ * argument's `redirect` property to one of the allowed values. Otherwise, if
43
+ * the returned fetch function is called with a `Request` object (and no
44
+ * explicit `redirect` init object), then the verification code will not be able
45
+ * to determine if the `redirect` property was explicitly set or based on the
46
+ * default value (`follow`), causing it to preventively block the request (throw
47
+ * an error). For this reason, unless you set
48
+ * {@link SafeFetchWrapOptions.allowImplicitRedirect} to `true`, you should
49
+ * **not** wrap the returned function into another function that creates a
50
+ * {@link Request} object before passing it to the function (as a e.g. a logging
51
+ * function would).
24
52
  */
25
- export function safeFetchWrap({
26
- fetch = globalThis.fetch as Fetch,
53
+ export function safeFetchWrap<C>({
54
+ fetch = globalThis.fetch as Fetch<C>,
55
+ dangerouslyForceKeepAliveAgent = false,
27
56
  responseMaxSize = 512 * 1024, // 512kB
28
57
  ssrfProtection = true,
29
58
  allowCustomPort = !ssrfProtection,
@@ -33,14 +62,8 @@ export function safeFetchWrap({
33
62
  allowPrivateIps = !ssrfProtection,
34
63
  timeout = 10e3,
35
64
  forbiddenDomainNames = DEFAULT_FORBIDDEN_DOMAIN_NAMES as Iterable<string>,
36
- /**
37
- * When `false`, a {@link RequestInit['redirect']} value must be explicitly
38
- * provided or the request will fail.
39
- *
40
- * @default false
41
- */
42
65
  allowImplicitRedirect = false,
43
- } = {}) {
66
+ }: SafeFetchWrapOptions<C> = {}) {
44
67
  return pipe(
45
68
  /**
46
69
  * Require explicit {@link RequestInit['redirect']} mode
@@ -83,7 +106,9 @@ export function safeFetchWrap({
83
106
  * input, we need to make sure that the request is not vulnerable to SSRF
84
107
  * attacks.
85
108
  */
86
- allowPrivateIps ? fetch : unicastFetchWrap({ fetch }),
109
+ allowPrivateIps
110
+ ? fetch
111
+ : unicastFetchWrap({ fetch, dangerouslyForceKeepAliveAgent }),
87
112
  ),
88
113
 
89
114
  /**
package/src/unicast.ts CHANGED
@@ -13,32 +13,55 @@ import { isUnicastIp } from './util.js'
13
13
 
14
14
  const { IPv4, IPv6 } = ipaddr
15
15
 
16
- export type SsrfFetchWrapOptions<C = FetchContext> = {
16
+ export type UnicastFetchWrapOptions<C = FetchContext> = {
17
17
  fetch?: Fetch<C>
18
+
19
+ /**
20
+ * ## ‼️ important security feature use with care
21
+ *
22
+ * On older NodeJS version, the `dispatcher` init option is ignored when
23
+ * creating a new Request instance. It can only be passed through the fetch
24
+ * function directly.
25
+ *
26
+ * Since this is a security feature, we need to ensure that the unicastLookup
27
+ * function is called to resolve the hostname to a unicast IP address.
28
+ *
29
+ * However, in the case a custom "fetch" function is passed here (fetch !==
30
+ * globalThis.fetch), we have no guarantee that the dispatcher will be used to
31
+ * make the request. Because of this, in such a case, we will use a one-time
32
+ * use dispatcher that checks that the provided fetch function indeed made use
33
+ * of the "unicastLookup" when a custom dispatch init function is used.
34
+ *
35
+ * Sadly, this means that we cannot use "keepAlive" connections, as the method
36
+ * used to ensure that "unicastLookup" gets called requires to create a new
37
+ * dispatcher for each request.
38
+ *
39
+ * If you can guarantee that the provided fetch function will make use of the
40
+ * "dispatcher" init option, you can set this flag to true, which will enable
41
+ * the use of a single agent (with keep-alive) for all requests.
42
+ *
43
+ * @default false
44
+ * @note This option has no effect on Node.js versions >= 20
45
+ */
46
+ dangerouslyForceKeepAliveAgent?: boolean
18
47
  }
19
48
 
49
+ // @TODO support other runtimes ?
50
+ const SUPPORTS_REQUEST_INIT_DISPATCHER =
51
+ Number(process.versions.node.split('.')[0]) >= 20
52
+
20
53
  /**
21
54
  * @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/}
22
55
  */
23
56
  export function unicastFetchWrap<C = FetchContext>({
24
57
  fetch = globalThis.fetch,
25
- }: SsrfFetchWrapOptions<C>): Fetch<C> {
26
- // In order to enforce the SSRF protection, we need to use a custom dispatcher
27
- // that uses "unicastLookup" to resolve the hostname to a unicast IP address.
28
-
29
- // In case a custom "fetch" function is passed here, we have no assurance that
30
- // the dispatcher will be used to make the request. Because of this, in case a
31
- // custom fetch method is passed, we will use a on-time use dispatcher that
32
- // ensures that "unicastLookup" gets called to resolve the hostname to an IP
33
- // address and ensure that it is a unicast address.
34
-
35
- // Sadly, this means that we cannot use "keepAlive" connections, as the method
36
- // used to ensure that "unicastLookup" gets called requires to create a new
37
- // dispatcher for each request.
38
-
39
- // @TODO: find a way to use a re-usable dispatcher with a custom fetch method.
40
-
41
- if (fetch === globalThis.fetch) {
58
+ dangerouslyForceKeepAliveAgent = false,
59
+ }: UnicastFetchWrapOptions<C>): Fetch<C> {
60
+ if (
61
+ SUPPORTS_REQUEST_INIT_DISPATCHER ||
62
+ dangerouslyForceKeepAliveAgent ||
63
+ fetch === globalThis.fetch
64
+ ) {
42
65
  const dispatcher = new Agent({
43
66
  connect: { lookup: unicastLookup },
44
67
  })
@@ -62,8 +85,14 @@ export function unicastFetchWrap<C = FetchContext>({
62
85
  )
63
86
  }
64
87
 
65
- // @ts-expect-error non-standard option
66
- return fetch.call(this, input, { ...init, dispatcher })
88
+ if (SUPPORTS_REQUEST_INIT_DISPATCHER) {
89
+ // @ts-expect-error non-standard option
90
+ const request = new Request(input, { ...init, dispatcher })
91
+ return fetch.call(this, request)
92
+ } else {
93
+ // @ts-expect-error non-standard option
94
+ return fetch.call(this, input, { ...init, dispatcher })
95
+ }
67
96
  }
68
97
  } else {
69
98
  return async function (input, init): Promise<Response> {
@@ -96,14 +125,16 @@ export function unicastFetchWrap<C = FetchContext>({
96
125
  }
97
126
 
98
127
  case undefined: {
99
- // hostname is a domain name, using the dispatcher defined above
100
- // will result in the DNS lookup being performed, ensuring that the
101
- // hostname resolves to a unicast address.
128
+ // hostname is a domain name, let's create a new dispatcher that
129
+ // will 1) use the unicastLookup function to resolve the hostname
130
+ // and 2) allow us to check that the lookup function was indeed
131
+ // called.
102
132
 
103
133
  let didLookup = false
104
134
  const dispatcher = new Client(url.origin, {
105
- // Do *not* enable H2 here, as it will cause an error (the client
106
- // will terminate the connection before the response is consumed).
135
+ // Do *not* enable H2 here, as it will cause an error (the
136
+ // client will terminate the connection before the response is
137
+ // consumed).
107
138
  // https://github.com/nodejs/undici/issues/3671
108
139
  connect: {
109
140
  keepAlive: false, // Client will be used once
@@ -198,7 +229,7 @@ export function unicastLookup(
198
229
  * @returns whether the hostname is a name typically used for on locale area networks.
199
230
  * @note **DO NOT** use for security reasons. Only as heuristic.
200
231
  */
201
- export function isLocalHostname(hostname: string): boolean {
232
+ function isLocalHostname(hostname: string): boolean {
202
233
  const parts = hostname.split('.')
203
234
  if (parts.length < 2) return true
204
235