@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 +14 -0
- package/dist/index.js.map +1 -1
- package/dist/safe.d.ts +33 -22
- package/dist/safe.d.ts.map +1 -1
- package/dist/safe.js +17 -9
- package/dist/safe.js.map +1 -1
- package/dist/unicast.d.ts +30 -8
- package/dist/unicast.d.ts.map +1 -1
- package/dist/unicast.js +22 -21
- package/dist/unicast.js.map +1 -1
- package/dist/util.js.map +1 -1
- package/package.json +1 -1
- package/src/safe.ts +39 -14
- package/src/unicast.ts +57 -26
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 {
|
|
2
|
-
export type SafeFetchWrapOptions =
|
|
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
|
-
* @
|
|
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
|
-
|
|
18
|
-
|
|
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
|
package/dist/safe.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"safe.d.ts","sourceRoot":"","sources":["../src/safe.ts"],"names":[],"mappings":"
|
|
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
|
-
* @
|
|
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
|
-
|
|
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
|
|
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":";;
|
|
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
|
|
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, }:
|
|
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
|
package/dist/unicast.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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,
|
|
65
|
-
// will
|
|
66
|
-
//
|
|
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
|
|
70
|
-
// will terminate the connection before the response is
|
|
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
|
package/dist/unicast.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unicast.js","sourceRoot":"","sources":["../src/unicast.ts"],"names":[],"mappings":";;;;;AAsBA,4CA4IC;AAED,sCA6BC;AAOD,0CAYC;AApND,wDAA6C;AAE7C,0DAA8B;AAC9B,mCAAsC;AACtC,+CAM4B;AAC5B,uCAAuC;AAEvC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,mBAAM,CAAA;AAM7B;;GAEG;AACH,SAAgB,gBAAgB,CAAmB,EACjD,KAAK,GAAG,UAAU,CAAC,KAAK,GACA;IACxB,8EAA8E;IAC9E,6EAA6E;IAE7E,8EAA8E;IAC9E,8EAA8E;IAC9E,2EAA2E;IAC3E,4EAA4E;IAC5E,mDAAmD;IAEnD,8EAA8E;IAC9E,2EAA2E;IAC3E,+BAA+B;IAE/B,8EAA8E;IAE9E,IAAI,KAAK,KAAK,UAAU,CAAC,KAAK,EAAE,CAAC;QAC/B,MAAM,UAAU,GAAG,IAAI,cAAK,CAAC;YAC3B,OAAO,EAAE,EAAE,MAAM,EAAE,aAAa,EAAE;SACnC,CAAC,CAAA;QAEF,OAAO,KAAK,WAAW,KAAK,EAAE,IAAI;YAChC,IAAI,IAAI,EAAE,UAAU,EAAE,CAAC;gBACrB,MAAM,IAAI,yBAAiB,CACzB,IAAA,iBAAS,EAAC,KAAK,EAAE,IAAI,CAAC,EACtB,GAAG,EACH,iEAAiE,CAClE,CAAA;YACH,CAAC;YAED,MAAM,GAAG,GAAG,IAAA,kBAAU,EAAC,KAAK,CAAC,CAAA;YAE7B,IAAI,GAAG,CAAC,QAAQ,IAAI,IAAA,qBAAW,EAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,KAAK,EAAE,CAAC;gBACxD,MAAM,IAAI,yBAAiB,CACzB,IAAA,iBAAS,EAAC,KAAK,EAAE,IAAI,CAAC,EACtB,GAAG,EACH,mCAAmC,CACpC,CAAA;YACH,CAAC;YAED,uCAAuC;YACvC,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,GAAG,IAAI,EAAE,UAAU,EAAE,CAAC,CAAA;QACzD,CAAC,CAAA;IACH,CAAC;SAAM,CAAC;QACN,OAAO,KAAK,WAAW,KAAK,EAAE,IAAI;YAChC,IAAI,IAAI,EAAE,UAAU,EAAE,CAAC;gBACrB,MAAM,IAAI,yBAAiB,CACzB,IAAA,iBAAS,EAAC,KAAK,EAAE,IAAI,CAAC,EACtB,GAAG,EACH,iEAAiE,CAClE,CAAA;YACH,CAAC;YAED,MAAM,GAAG,GAAG,IAAA,kBAAU,EAAC,KAAK,CAAC,CAAA;YAE7B,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;gBAClB,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA;YACtC,CAAC;YAED,QAAQ,IAAA,qBAAW,EAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;gBAClC,KAAK,IAAI,CAAC,CAAC,CAAC;oBACV,kDAAkD;oBAClD,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA;gBACtC,CAAC;gBAED,KAAK,KAAK,CAAC,CAAC,CAAC;oBACX,MAAM,IAAI,yBAAiB,CACzB,IAAA,iBAAS,EAAC,KAAK,EAAE,IAAI,CAAC,EACtB,GAAG,EACH,mCAAmC,CACpC,CAAA;gBACH,CAAC;gBAED,KAAK,SAAS,CAAC,CAAC,CAAC;oBACf,gEAAgE;oBAChE,mEAAmE;oBACnE,0CAA0C;oBAE1C,IAAI,SAAS,GAAG,KAAK,CAAA;oBACrB,MAAM,UAAU,GAAG,IAAI,eAAM,CAAC,GAAG,CAAC,MAAM,EAAE;wBACxC,iEAAiE;wBACjE,kEAAkE;wBAClE,+CAA+C;wBAC/C,OAAO,EAAE;4BACP,SAAS,EAAE,KAAK,EAAE,2BAA2B;4BAC7C,MAAM,CAAC,GAAG,IAAI;gCACZ,SAAS,GAAG,IAAI,CAAA;gCAChB,aAAa,CAAC,GAAG,IAAI,CAAC,CAAA;4BACxB,CAAC;yBACF;qBACF,CAAC,CAAA;oBAEF,IAAI,CAAC;wBACH,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAA;wBAC1C,OAAO,CAAC,GAAG,CAAC,YAAY,EAAE,OAAO,CAAC,CAAA,CAAC,mCAAmC;wBAEtE,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;4BAC7C,GAAG,IAAI;4BACP,OAAO;4BACP,uCAAuC;4BACvC,UAAU;yBACX,CAAC,CAAA;wBAEF,IAAI,CAAC,SAAS,EAAE,CAAC;4BACf,8DAA8D;4BAC9D,+DAA+D;4BAC/D,kEAAkE;4BAClE,4DAA4D;4BAC5D,8CAA8C;4BAC9C,EAAE;4BACF,qDAAqD;4BACrD,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAA;4BAE7B,iEAAiE;4BACjE,+DAA+D;4BAC/D,iDAAiD;4BAEjD,6CAA6C;4BAC7C,MAAM,IAAI,yBAAiB,CACzB,IAAA,iBAAS,EAAC,KAAK,EAAE,IAAI,CAAC,EACtB,GAAG,EACH,mCAAmC,CACpC,CAAA;wBACH,CAAC;wBAED,OAAO,QAAQ,CAAA;oBACjB,CAAC;4BAAS,CAAC;wBACT,kEAAkE;wBAClE,iBAAiB;wBACjB,KAAK,UAAU,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;4BACpC,oCAAoC;4BACpC,OAAO,CAAC,IAAI,CAAC,4BAA4B,EAAE,GAAG,CAAC,CAAA;wBACjD,CAAC,CAAC,CAAA;oBACJ,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC,CAAA;IACH,CAAC;AACH,CAAC;AAED,SAAgB,aAAa,CAC3B,QAAgB,EAChB,OAA0B,EAC1B,QAAuC;IAEvC,IAAI,eAAe,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC9B,QAAQ,CAAC,IAAI,KAAK,CAAC,iCAAiC,CAAC,EAAE,EAAE,CAAC,CAAA;QAC1D,OAAM;IACR,CAAC;IAED,kBAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE;QACrD,IAAI,GAAG,EAAE,CAAC;YACR,QAAQ,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;QAChC,CAAC;aAAM,CAAC;YACN,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC;gBAChC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC;gBACjC,CAAC,CAAC,CAAC,kBAAkB,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,CAAA;YAE7C,IAAI,GAAG,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;gBAC3B,QAAQ,CACN,IAAI,KAAK,CAAC,0CAA0C,CAAC,EACrD,OAAO,EACP,MAAM,CACP,CAAA;YACH,CAAC;iBAAM,CAAC;gBACN,QAAQ,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;YACjC,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAA;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAgB,eAAe,CAAC,QAAgB;IAC9C,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IACjC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,IAAI,CAAA;IAEjC,MAAM,GAAG,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,WAAW,EAAE,CAAA;IACvC,OAAO,CACL,GAAG,KAAK,MAAM;QACd,GAAG,KAAK,OAAO;QACf,GAAG,KAAK,WAAW;QACnB,GAAG,KAAK,SAAS;QACjB,GAAG,KAAK,SAAS,CAClB,CAAA;AACH,CAAC;AAED,SAAS,YAAY,CAAC,EAA6B;IACjD,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,SAAS,CAAA;AACjC,CAAC;AAED,SAAS,kBAAkB,CAAC,EAC1B,OAAO,EACP,MAAM,GACQ;IACd,MAAM,EAAE,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IAEnE,IAAI,EAAE,YAAY,IAAI,IAAI,EAAE,CAAC,mBAAmB,EAAE,EAAE,CAAC;QACnD,OAAO,EAAE,CAAC,aAAa,EAAE,CAAA;IAC3B,CAAC;SAAM,CAAC;QACN,OAAO,EAAE,CAAA;IACX,CAAC;AACH,CAAC"}
|
|
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
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 =
|
|
16
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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,
|
|
100
|
-
// will
|
|
101
|
-
//
|
|
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
|
|
106
|
-
// will terminate the connection before the response is
|
|
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
|
-
|
|
232
|
+
function isLocalHostname(hostname: string): boolean {
|
|
202
233
|
const parts = hostname.split('.')
|
|
203
234
|
if (parts.length < 2) return true
|
|
204
235
|
|