@atproto-labs/fetch-node 0.1.9 → 0.1.10

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,13 @@
1
1
  # @atproto-labs/fetch-node
2
2
 
3
+ ## 0.1.10
4
+
5
+ ### Patch Changes
6
+
7
+ - [#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
8
+
9
+ - [#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
10
+
3
11
  ## 0.1.9
4
12
 
5
13
  ### Patch Changes
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"}
package/dist/unicast.d.ts CHANGED
@@ -1,13 +1,41 @@
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
40
  /**
13
41
  * @param hostname - a syntactically valid hostname
@@ -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;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAYzD"}
package/dist/unicast.js CHANGED
@@ -12,22 +12,15 @@ const undici_1 = require("undici");
12
12
  const fetch_1 = require("@atproto-labs/fetch");
13
13
  const util_js_1 = require("./util.js");
14
14
  const { IPv4, IPv6 } = ipaddr_js_1.default;
15
+ // @TODO support other runtimes ?
16
+ const SUPPORTS_REQUEST_INIT_DISPATCHER = Number(process.versions.node.split('.')[0]) >= 20;
15
17
  /**
16
18
  * @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/}
17
19
  */
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) {
20
+ function unicastFetchWrap({ fetch = globalThis.fetch, dangerouslyForceKeepAliveAgent = false, }) {
21
+ if (SUPPORTS_REQUEST_INIT_DISPATCHER ||
22
+ dangerouslyForceKeepAliveAgent ||
23
+ fetch === globalThis.fetch) {
31
24
  const dispatcher = new undici_1.Agent({
32
25
  connect: { lookup: unicastLookup },
33
26
  });
@@ -39,8 +32,15 @@ function unicastFetchWrap({ fetch = globalThis.fetch, }) {
39
32
  if (url.hostname && (0, util_js_1.isUnicastIp)(url.hostname) === false) {
40
33
  throw new fetch_1.FetchRequestError((0, fetch_1.asRequest)(input, init), 400, 'Hostname is a non-unicast address');
41
34
  }
42
- // @ts-expect-error non-standard option
43
- return fetch.call(this, input, { ...init, dispatcher });
35
+ if (SUPPORTS_REQUEST_INIT_DISPATCHER) {
36
+ // @ts-expect-error non-standard option
37
+ const request = new Request(input, { ...init, dispatcher });
38
+ return fetch.call(this, request);
39
+ }
40
+ else {
41
+ // @ts-expect-error non-standard option
42
+ return fetch.call(this, input, { ...init, dispatcher });
43
+ }
44
44
  };
45
45
  }
46
46
  else {
@@ -61,13 +61,15 @@ function unicastFetchWrap({ fetch = globalThis.fetch, }) {
61
61
  throw new fetch_1.FetchRequestError((0, fetch_1.asRequest)(input, init), 400, 'Hostname is a non-unicast address');
62
62
  }
63
63
  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.
64
+ // hostname is a domain name, let's create a new dispatcher that
65
+ // will 1) use the unicastLookup function to resolve the hostname
66
+ // and 2) allow us to check that the lookup function was indeed
67
+ // called.
67
68
  let didLookup = false;
68
69
  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).
70
+ // Do *not* enable H2 here, as it will cause an error (the
71
+ // client will terminate the connection before the response is
72
+ // consumed).
71
73
  // https://github.com/nodejs/undici/issues/3671
72
74
  connect: {
73
75
  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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"}
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.1.10",
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