@atproto-labs/fetch-node 0.1.0 → 0.1.1

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,16 @@
1
1
  # @atproto-labs/fetch-node
2
2
 
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Prevent bypass of ssrf ip verification
8
+
9
+ - [#2770](https://github.com/bluesky-social/atproto/pull/2770) [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Expose IP filtering utilities
10
+
11
+ - Updated dependencies [[`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3), [`a07b21151`](https://github.com/bluesky-social/atproto/commit/a07b21151f1850340c4b7797ebb11521b1a6cdf3)]:
12
+ - @atproto-labs/fetch@0.1.1
13
+
3
14
  ## 0.1.0
4
15
 
5
16
  ### Minor Changes
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from '@atproto-labs/fetch';
2
2
  export * from './safe.js';
3
- export * from './ssrf.js';
3
+ export * from './unicast.js';
4
+ export * from './util.js';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AAEnC,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AAEnC,cAAc,WAAW,CAAA;AACzB,cAAc,cAAc,CAAA;AAC5B,cAAc,WAAW,CAAA"}
package/dist/index.js CHANGED
@@ -16,5 +16,6 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("@atproto-labs/fetch"), exports);
18
18
  __exportStar(require("./safe.js"), exports);
19
- __exportStar(require("./ssrf.js"), exports);
19
+ __exportStar(require("./unicast.js"), exports);
20
+ __exportStar(require("./util.js"), exports);
20
21
  //# sourceMappingURL=index.js.map
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,4CAAyB"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,sDAAmC;AAEnC,4CAAyB;AACzB,+CAA4B;AAC5B,4CAAyB"}
package/dist/safe.d.ts CHANGED
@@ -3,14 +3,19 @@ export type SafeFetchWrapOptions = NonNullable<Parameters<typeof safeFetchWrap>[
3
3
  /**
4
4
  * Wrap a fetch function with safety checks so that it can be safely used
5
5
  * with user provided input (URL).
6
+ *
7
+ * @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html}
6
8
  */
7
9
  export declare function safeFetchWrap({ fetch, responseMaxSize, // 512kB
8
- allowHttp, allowData, ssrfProtection, timeout, forbiddenDomainNames, }?: {
10
+ ssrfProtection, allowCustomPort, allowData, allowHttp, allowIpHost, allowPrivateIps, timeout, forbiddenDomainNames, }?: {
9
11
  fetch?: Fetch | undefined;
10
12
  responseMaxSize?: number | undefined;
11
- allowHttp?: boolean | undefined;
12
- allowData?: boolean | undefined;
13
13
  ssrfProtection?: boolean | undefined;
14
+ allowCustomPort?: boolean | undefined;
15
+ allowData?: boolean | undefined;
16
+ allowHttp?: boolean | undefined;
17
+ allowIpHost?: boolean | undefined;
18
+ allowPrivateIps?: boolean | undefined;
14
19
  timeout?: number | undefined;
15
20
  forbiddenDomainNames?: Iterable<string> | undefined;
16
21
  }): Fetch<unknown>;
@@ -1 +1 @@
1
- {"version":3,"file":"safe.d.ts","sourceRoot":"","sources":["../src/safe.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,EAON,MAAM,qBAAqB,CAAA;AAK5B,MAAM,MAAM,oBAAoB,GAAG,WAAW,CAC5C,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC,CAAC,CAAC,CACpC,CAAA;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAC5B,KAAiC,EACjC,eAA4B,EAAE,QAAQ;AACtC,SAAiB,EACjB,SAAiB,EACjB,cAAqB,EACrB,OAAc,EACd,oBAAyE,GAC1E;;;;;;;;CAAK,GAAG,KAAK,CAAC,OAAO,CAAC,CA+CtB"}
1
+ {"version":3,"file":"safe.d.ts","sourceRoot":"","sources":["../src/safe.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,EAQN,MAAM,qBAAqB,CAAA;AAK5B,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,GAC1E;;;;;;;;;;;CAAK,GAAG,KAAK,CAAC,OAAO,CAAC,CAsDtB"}
package/dist/safe.js CHANGED
@@ -3,24 +3,34 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.safeFetchWrap = void 0;
4
4
  const fetch_1 = require("@atproto-labs/fetch");
5
5
  const pipe_1 = require("@atproto-labs/pipe");
6
- const ssrf_js_1 = require("./ssrf.js");
6
+ const unicast_js_1 = require("./unicast.js");
7
7
  /**
8
8
  * Wrap a fetch function with safety checks so that it can be safely used
9
9
  * with user provided input (URL).
10
+ *
11
+ * @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html}
10
12
  */
11
13
  function safeFetchWrap({ fetch = globalThis.fetch, responseMaxSize = 512 * 1024, // 512kB
12
- allowHttp = false, allowData = false, ssrfProtection = true, timeout = 10e3, forbiddenDomainNames = fetch_1.DEFAULT_FORBIDDEN_DOMAIN_NAMES, } = {}) {
14
+ ssrfProtection = true, allowCustomPort = !ssrfProtection, allowData = false, allowHttp = !ssrfProtection, allowIpHost = true, allowPrivateIps = !ssrfProtection, timeout = 10e3, forbiddenDomainNames = fetch_1.DEFAULT_FORBIDDEN_DOMAIN_NAMES, } = {}) {
13
15
  return (0, fetch_1.toRequestTransformer)((0, pipe_1.pipe)(
14
16
  /**
15
- * Prevent using http:, file: or data: protocols.
17
+ * Disable HTTP redirects
16
18
  */
17
- (0, fetch_1.protocolCheckRequestTransform)(['https:']
18
- .concat(allowHttp ? ['http:'] : [])
19
- .concat(allowData ? ['data:'] : [])),
19
+ (0, fetch_1.redirectCheckRequestTransform)(),
20
20
  /**
21
21
  * Only requests that will be issued with a "Host" header are allowed.
22
22
  */
23
- (0, fetch_1.requireHostHeaderTranform)(),
23
+ allowIpHost ? fetch_1.asRequest : (0, fetch_1.requireHostHeaderTransform)(),
24
+ /**
25
+ * Prevent using http:, file: or data: protocols.
26
+ */
27
+ (0, fetch_1.protocolCheckRequestTransform)({
28
+ 'about:': false,
29
+ 'data:': allowData,
30
+ 'file:': false,
31
+ 'http:': allowHttp && { allowCustomPort },
32
+ 'https:': { allowCustomPort },
33
+ }),
24
34
  /**
25
35
  * Disallow fetching from domains we know are not atproto/OIDC client
26
36
  * implementation. Note that other domains can be blocked by providing a
@@ -38,7 +48,7 @@ allowHttp = false, allowData = false, ssrfProtection = true, timeout = 10e3, for
38
48
  * input, we need to make sure that the request is not vulnerable to SSRF
39
49
  * attacks.
40
50
  */
41
- ssrfProtection ? (0, ssrf_js_1.ssrfFetchWrap)({ fetch }) : fetch),
51
+ allowPrivateIps ? fetch : (0, unicast_js_1.unicastFetchWrap)({ fetch })),
42
52
  /**
43
53
  * Since we will be fetching user owned data, we need to make sure that an
44
54
  * 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":";;;AAAA,+CAS4B;AAC5B,6CAAyC;AAEzC,uCAAyC;AAMzC;;;GAGG;AACH,SAAgB,aAAa,CAAC,EAC5B,KAAK,GAAG,UAAU,CAAC,KAAc,EACjC,eAAe,GAAG,GAAG,GAAG,IAAI,EAAE,QAAQ;AACtC,SAAS,GAAG,KAAK,EACjB,SAAS,GAAG,KAAK,EACjB,cAAc,GAAG,IAAI,EACrB,OAAO,GAAG,IAAI,EACd,oBAAoB,GAAG,sCAAkD,MACvE,EAAE;IACJ,OAAO,IAAA,4BAAoB,EACzB,IAAA,WAAI;IACF;;OAEG;IACH,IAAA,qCAA6B,EAC3B,CAAC,QAAQ,CAAC;SACP,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;SAClC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CACtC;IAED;;OAEG;IACH,IAAA,iCAAyB,GAAE;IAE3B;;;;;OAKG;IACH,IAAA,2CAAmC,EAAC,oBAAoB,CAAC;IAEzD;;;OAGG;IACH,IAAA,kBAAU,EACR,OAAO;IAEP;;;;OAIG;IACH,cAAc,CAAC,CAAC,CAAC,IAAA,uBAAa,EAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,CAClD;IAED;;;OAGG;IACH,IAAA,6BAAqB,EAAC,eAAe,CAAC,CACvC,CACF,CAAA;AACH,CAAC;AAvDD,sCAuDC"}
1
+ {"version":3,"file":"safe.js","sourceRoot":"","sources":["../src/safe.ts"],"names":[],"mappings":";;;AAAA,+CAW4B;AAC5B,6CAAyC;AAEzC,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,MACvE,EAAE;IACJ,OAAO,IAAA,4BAAoB,EACzB,IAAA,WAAI;IACF;;OAEG;IACH,IAAA,qCAA6B,GAAE;IAE/B;;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,CACvC,CACF,CAAA;AACH,CAAC;AAjED,sCAiEC"}
@@ -0,0 +1,14 @@
1
+ /// <reference types="node" />
2
+ /// <reference types="node" />
3
+ import dns from 'node:dns';
4
+ import { LookupFunction } from 'node:net';
5
+ import { Fetch, FetchContext } from '@atproto-labs/fetch';
6
+ export type SsrfFetchWrapOptions<C = FetchContext> = {
7
+ fetch?: Fetch<C>;
8
+ };
9
+ /**
10
+ * @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/}
11
+ */
12
+ export declare function unicastFetchWrap<C = FetchContext>({ fetch, }: SsrfFetchWrapOptions<C>): Fetch<C>;
13
+ export declare function unicastLookup(hostname: string, options: dns.LookupOptions, callback: Parameters<LookupFunction>[2]): void;
14
+ //# sourceMappingURL=unicast.d.ts.map
@@ -0,0 +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;AAEzC,OAAO,EAGL,KAAK,EACL,YAAY,EAEb,MAAM,qBAAqB,CAAA;AAS5B,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,CA8HpC;AAED,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,GAAG,CAAC,aAAa,EAC1B,QAAQ,EAAE,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,QA0BxC"}
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.unicastLookup = exports.unicastFetchWrap = void 0;
7
+ const node_dns_1 = __importDefault(require("node:dns"));
8
+ const fetch_1 = require("@atproto-labs/fetch");
9
+ const ipaddr_js_1 = __importDefault(require("ipaddr.js"));
10
+ const psl_1 = require("psl");
11
+ const undici_1 = require("undici");
12
+ const util_js_1 = require("./util.js");
13
+ const { IPv4, IPv6 } = ipaddr_js_1.default;
14
+ /**
15
+ * @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/}
16
+ */
17
+ function unicastFetchWrap({ fetch = globalThis.fetch, }) {
18
+ // In order to enforce the SSRF protection, we need to use a custom dispatcher
19
+ // that uses "unicastLookup" to resolve the hostname to a unicast IP address.
20
+ // In case a custom "fetch" function is passed here, we have no assurance that
21
+ // the dispatcher will be used to make the request. Because of this, in case a
22
+ // custom fetch method is passed, we will use a on-time use dispatcher that
23
+ // ensures that "unicastLookup" gets called to resolve the hostname to an IP
24
+ // address and ensure that it is a unicast address.
25
+ // Sadly, this means that we cannot use "keepAlive" connections, as the method
26
+ // used to ensure that "unicastLookup" gets called requires to create a new
27
+ // dispatcher for each request.
28
+ // @TODO: find a way to use a re-usable dispatcher with a custom fetch method.
29
+ if (fetch === globalThis.fetch) {
30
+ const dispatcher = new undici_1.Agent({
31
+ allowH2: true,
32
+ connect: { keepAlive: true, lookup: unicastLookup },
33
+ });
34
+ return async function (input, init) {
35
+ if (init?.dispatcher) {
36
+ throw new fetch_1.FetchRequestError((0, fetch_1.asRequest)(input, init), 500, 'SSRF protection cannot be used with a custom request dispatcher');
37
+ }
38
+ const url = (0, fetch_1.extractUrl)(input);
39
+ if (url.hostname && (0, util_js_1.isUnicastIp)(url.hostname) === false) {
40
+ throw new fetch_1.FetchRequestError((0, fetch_1.asRequest)(input, init), 400, 'Hostname is a non-unicast address');
41
+ }
42
+ // @ts-expect-error non-standard option
43
+ return fetch.call(this, input, { ...init, dispatcher });
44
+ };
45
+ }
46
+ else {
47
+ return async function (input, init) {
48
+ if (init?.dispatcher) {
49
+ throw new fetch_1.FetchRequestError((0, fetch_1.asRequest)(input, init), 500, 'SSRF protection cannot be used with a custom request dispatcher');
50
+ }
51
+ const url = (0, fetch_1.extractUrl)(input);
52
+ if (!url.hostname) {
53
+ return fetch.call(this, input, init);
54
+ }
55
+ switch ((0, util_js_1.isUnicastIp)(url.hostname)) {
56
+ case true: {
57
+ // hostname is a unicast address, safe to proceed.
58
+ return fetch.call(this, input, init);
59
+ }
60
+ case false: {
61
+ throw new fetch_1.FetchRequestError((0, fetch_1.asRequest)(input, init), 400, 'Hostname is a non-unicast address');
62
+ }
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.
67
+ let didLookup = false;
68
+ const dispatcher = new undici_1.Client(url.origin, {
69
+ allowH2: true,
70
+ connect: {
71
+ keepAlive: false, // Client will be used once
72
+ lookup(...args) {
73
+ didLookup = true;
74
+ unicastLookup(...args);
75
+ },
76
+ },
77
+ });
78
+ const headers = new Headers(init?.headers);
79
+ headers.set('connection', 'close'); // Proactively close the connection
80
+ try {
81
+ return await fetch.call(this, input, {
82
+ ...init,
83
+ headers,
84
+ // @ts-expect-error non-standard option
85
+ dispatcher,
86
+ });
87
+ }
88
+ finally {
89
+ // Free resources (we cannot await here since the response was not
90
+ // consumed yet).
91
+ void dispatcher.close().catch((err) => {
92
+ // No biggie, but let's still log it
93
+ console.warn('Failed to close dispatcher', err);
94
+ });
95
+ if (!didLookup) {
96
+ // If you encounter this error, either upgrade to Node.js >=21 or
97
+ // make sure that the dispatcher passed through the requestInit
98
+ // object ends up being used to make the request.
99
+ // eslint-disable-next-line no-unsafe-finally
100
+ throw new fetch_1.FetchRequestError((0, fetch_1.asRequest)(input, init), 500, 'Unable to enforce SSRF protection');
101
+ }
102
+ }
103
+ }
104
+ }
105
+ };
106
+ }
107
+ }
108
+ exports.unicastFetchWrap = unicastFetchWrap;
109
+ function unicastLookup(hostname, options, callback) {
110
+ if (!(0, psl_1.isValid)(hostname)) {
111
+ callback(new Error('Hostname is not a public domain'), '');
112
+ return;
113
+ }
114
+ node_dns_1.default.lookup(hostname, options, (err, address, family) => {
115
+ if (err) {
116
+ callback(err, address, family);
117
+ }
118
+ else {
119
+ const ips = Array.isArray(address)
120
+ ? address.map(parseLookupAddress)
121
+ : [parseLookupAddress({ address, family })];
122
+ if (ips.some(isNotUnicast)) {
123
+ callback(new Error('Hostname resolved to non-unicast address'), address, family);
124
+ }
125
+ else {
126
+ callback(null, address, family);
127
+ }
128
+ }
129
+ });
130
+ }
131
+ exports.unicastLookup = unicastLookup;
132
+ function isNotUnicast(ip) {
133
+ return ip.range() !== 'unicast';
134
+ }
135
+ function parseLookupAddress({ address, family, }) {
136
+ const ip = family === 4 ? IPv4.parse(address) : IPv6.parse(address);
137
+ if (ip instanceof IPv6 && ip.isIPv4MappedAddress()) {
138
+ return ip.toIPv4Address();
139
+ }
140
+ else {
141
+ return ip;
142
+ }
143
+ }
144
+ //# sourceMappingURL=unicast.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unicast.js","sourceRoot":"","sources":["../src/unicast.ts"],"names":[],"mappings":"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"}
package/dist/util.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function isUnicastIp(hostname: string): boolean | undefined;
2
+ //# sourceMappingURL=util.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAkBA,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,CAGjE"}
package/dist/util.js ADDED
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.isUnicastIp = void 0;
7
+ const ipaddr_js_1 = __importDefault(require("ipaddr.js"));
8
+ const { IPv4, IPv6 } = ipaddr_js_1.default;
9
+ function parseIpHostname(hostname) {
10
+ if (IPv4.isIPv4(hostname)) {
11
+ return IPv4.parse(hostname);
12
+ }
13
+ if (hostname.startsWith('[') && hostname.endsWith(']')) {
14
+ return IPv6.parse(hostname.slice(1, -1));
15
+ }
16
+ return undefined;
17
+ }
18
+ function isUnicastIp(hostname) {
19
+ const ip = parseIpHostname(hostname);
20
+ return ip ? ip.range() === 'unicast' : undefined;
21
+ }
22
+ exports.isUnicastIp = isUnicastIp;
23
+ //# sourceMappingURL=util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":";;;;;;AAAA,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;AAHD,kCAGC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atproto-labs/fetch-node",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "license": "MIT",
5
5
  "description": "SSRF protection for fetch() in Node.js",
6
6
  "keywords": [
@@ -27,7 +27,7 @@
27
27
  "ipaddr.js": "^2.1.0",
28
28
  "psl": "^1.9.0",
29
29
  "undici": "^6.14.1",
30
- "@atproto-labs/fetch": "0.1.0",
30
+ "@atproto-labs/fetch": "0.1.1",
31
31
  "@atproto-labs/pipe": "0.1.0"
32
32
  },
33
33
  "devDependencies": {
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from '@atproto-labs/fetch'
2
2
 
3
3
  export * from './safe.js'
4
- export * from './ssrf.js'
4
+ export * from './unicast.js'
5
+ export * from './util.js'
package/src/safe.ts CHANGED
@@ -1,16 +1,18 @@
1
1
  import {
2
+ asRequest,
2
3
  DEFAULT_FORBIDDEN_DOMAIN_NAMES,
3
4
  Fetch,
4
5
  fetchMaxSizeProcessor,
5
6
  forbiddenDomainNameRequestTransform,
6
7
  protocolCheckRequestTransform,
7
- requireHostHeaderTranform,
8
+ redirectCheckRequestTransform,
9
+ requireHostHeaderTransform,
8
10
  timedFetch,
9
11
  toRequestTransformer,
10
12
  } from '@atproto-labs/fetch'
11
13
  import { pipe } from '@atproto-labs/pipe'
12
14
 
13
- import { ssrfFetchWrap } from './ssrf.js'
15
+ import { unicastFetchWrap } from './unicast.js'
14
16
 
15
17
  export type SafeFetchWrapOptions = NonNullable<
16
18
  Parameters<typeof safeFetchWrap>[0]
@@ -19,31 +21,43 @@ export type SafeFetchWrapOptions = NonNullable<
19
21
  /**
20
22
  * Wrap a fetch function with safety checks so that it can be safely used
21
23
  * with user provided input (URL).
24
+ *
25
+ * @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html}
22
26
  */
23
27
  export function safeFetchWrap({
24
28
  fetch = globalThis.fetch as Fetch,
25
29
  responseMaxSize = 512 * 1024, // 512kB
26
- allowHttp = false,
27
- allowData = false,
28
30
  ssrfProtection = true,
31
+ allowCustomPort = !ssrfProtection,
32
+ allowData = false,
33
+ allowHttp = !ssrfProtection,
34
+ allowIpHost = true,
35
+ allowPrivateIps = !ssrfProtection,
29
36
  timeout = 10e3,
30
37
  forbiddenDomainNames = DEFAULT_FORBIDDEN_DOMAIN_NAMES as Iterable<string>,
31
38
  } = {}): Fetch<unknown> {
32
39
  return toRequestTransformer(
33
40
  pipe(
34
41
  /**
35
- * Prevent using http:, file: or data: protocols.
42
+ * Disable HTTP redirects
36
43
  */
37
- protocolCheckRequestTransform(
38
- ['https:']
39
- .concat(allowHttp ? ['http:'] : [])
40
- .concat(allowData ? ['data:'] : []),
41
- ),
44
+ redirectCheckRequestTransform(),
42
45
 
43
46
  /**
44
47
  * Only requests that will be issued with a "Host" header are allowed.
45
48
  */
46
- requireHostHeaderTranform(),
49
+ allowIpHost ? asRequest : requireHostHeaderTransform(),
50
+
51
+ /**
52
+ * Prevent using http:, file: or data: protocols.
53
+ */
54
+ protocolCheckRequestTransform({
55
+ 'about:': false,
56
+ 'data:': allowData,
57
+ 'file:': false,
58
+ 'http:': allowHttp && { allowCustomPort },
59
+ 'https:': { allowCustomPort },
60
+ }),
47
61
 
48
62
  /**
49
63
  * Disallow fetching from domains we know are not atproto/OIDC client
@@ -65,7 +79,7 @@ export function safeFetchWrap({
65
79
  * input, we need to make sure that the request is not vulnerable to SSRF
66
80
  * attacks.
67
81
  */
68
- ssrfProtection ? ssrfFetchWrap({ fetch }) : fetch,
82
+ allowPrivateIps ? fetch : unicastFetchWrap({ fetch }),
69
83
  ),
70
84
 
71
85
  /**
package/src/unicast.ts ADDED
@@ -0,0 +1,202 @@
1
+ import dns, { LookupAddress } from 'node:dns'
2
+ import { LookupFunction } from 'node:net'
3
+
4
+ import {
5
+ asRequest,
6
+ extractUrl,
7
+ Fetch,
8
+ FetchContext,
9
+ FetchRequestError,
10
+ } from '@atproto-labs/fetch'
11
+ import ipaddr from 'ipaddr.js'
12
+ import { isValid as isValidDomain } from 'psl'
13
+ import { Agent, Client } from 'undici'
14
+
15
+ import { isUnicastIp } from './util.js'
16
+
17
+ const { IPv4, IPv6 } = ipaddr
18
+
19
+ export type SsrfFetchWrapOptions<C = FetchContext> = {
20
+ fetch?: Fetch<C>
21
+ }
22
+
23
+ /**
24
+ * @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/}
25
+ */
26
+ export function unicastFetchWrap<C = FetchContext>({
27
+ fetch = globalThis.fetch,
28
+ }: SsrfFetchWrapOptions<C>): Fetch<C> {
29
+ // In order to enforce the SSRF protection, we need to use a custom dispatcher
30
+ // that uses "unicastLookup" to resolve the hostname to a unicast IP address.
31
+
32
+ // In case a custom "fetch" function is passed here, we have no assurance that
33
+ // the dispatcher will be used to make the request. Because of this, in case a
34
+ // custom fetch method is passed, we will use a on-time use dispatcher that
35
+ // ensures that "unicastLookup" gets called to resolve the hostname to an IP
36
+ // address and ensure that it is a unicast address.
37
+
38
+ // Sadly, this means that we cannot use "keepAlive" connections, as the method
39
+ // used to ensure that "unicastLookup" gets called requires to create a new
40
+ // dispatcher for each request.
41
+
42
+ // @TODO: find a way to use a re-usable dispatcher with a custom fetch method.
43
+
44
+ if (fetch === globalThis.fetch) {
45
+ const dispatcher = new Agent({
46
+ allowH2: true,
47
+ connect: { keepAlive: true, lookup: unicastLookup },
48
+ })
49
+
50
+ return async function (input, init): Promise<Response> {
51
+ if (init?.dispatcher) {
52
+ throw new FetchRequestError(
53
+ asRequest(input, init),
54
+ 500,
55
+ 'SSRF protection cannot be used with a custom request dispatcher',
56
+ )
57
+ }
58
+
59
+ const url = extractUrl(input)
60
+
61
+ if (url.hostname && isUnicastIp(url.hostname) === false) {
62
+ throw new FetchRequestError(
63
+ asRequest(input, init),
64
+ 400,
65
+ 'Hostname is a non-unicast address',
66
+ )
67
+ }
68
+
69
+ // @ts-expect-error non-standard option
70
+ return fetch.call(this, input, { ...init, dispatcher })
71
+ }
72
+ } else {
73
+ return async function (input, init): Promise<Response> {
74
+ if (init?.dispatcher) {
75
+ throw new FetchRequestError(
76
+ asRequest(input, init),
77
+ 500,
78
+ 'SSRF protection cannot be used with a custom request dispatcher',
79
+ )
80
+ }
81
+
82
+ const url = extractUrl(input)
83
+
84
+ if (!url.hostname) {
85
+ return fetch.call(this, input, init)
86
+ }
87
+
88
+ switch (isUnicastIp(url.hostname)) {
89
+ case true: {
90
+ // hostname is a unicast address, safe to proceed.
91
+ return fetch.call(this, input, init)
92
+ }
93
+
94
+ case false: {
95
+ throw new FetchRequestError(
96
+ asRequest(input, init),
97
+ 400,
98
+ 'Hostname is a non-unicast address',
99
+ )
100
+ }
101
+
102
+ case undefined: {
103
+ // hostname is a domain name, using the dispatcher defined above
104
+ // will result in the DNS lookup being performed, ensuring that the
105
+ // hostname resolves to a unicast address.
106
+
107
+ let didLookup = false
108
+ const dispatcher = new Client(url.origin, {
109
+ allowH2: true,
110
+ connect: {
111
+ keepAlive: false, // Client will be used once
112
+ lookup(...args) {
113
+ didLookup = true
114
+ unicastLookup(...args)
115
+ },
116
+ },
117
+ })
118
+
119
+ const headers = new Headers(init?.headers)
120
+ headers.set('connection', 'close') // Proactively close the connection
121
+
122
+ try {
123
+ return await fetch.call(this, input, {
124
+ ...init,
125
+ headers,
126
+ // @ts-expect-error non-standard option
127
+ dispatcher,
128
+ })
129
+ } finally {
130
+ // Free resources (we cannot await here since the response was not
131
+ // consumed yet).
132
+ void dispatcher.close().catch((err) => {
133
+ // No biggie, but let's still log it
134
+ console.warn('Failed to close dispatcher', err)
135
+ })
136
+
137
+ if (!didLookup) {
138
+ // If you encounter this error, either upgrade to Node.js >=21 or
139
+ // make sure that the dispatcher passed through the requestInit
140
+ // object ends up being used to make the request.
141
+
142
+ // eslint-disable-next-line no-unsafe-finally
143
+ throw new FetchRequestError(
144
+ asRequest(input, init),
145
+ 500,
146
+ 'Unable to enforce SSRF protection',
147
+ )
148
+ }
149
+ }
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
155
+
156
+ export function unicastLookup(
157
+ hostname: string,
158
+ options: dns.LookupOptions,
159
+ callback: Parameters<LookupFunction>[2],
160
+ ) {
161
+ if (!isValidDomain(hostname)) {
162
+ callback(new Error('Hostname is not a public domain'), '')
163
+ return
164
+ }
165
+
166
+ dns.lookup(hostname, options, (err, address, family) => {
167
+ if (err) {
168
+ callback(err, address, family)
169
+ } else {
170
+ const ips = Array.isArray(address)
171
+ ? address.map(parseLookupAddress)
172
+ : [parseLookupAddress({ address, family })]
173
+
174
+ if (ips.some(isNotUnicast)) {
175
+ callback(
176
+ new Error('Hostname resolved to non-unicast address'),
177
+ address,
178
+ family,
179
+ )
180
+ } else {
181
+ callback(null, address, family)
182
+ }
183
+ }
184
+ })
185
+ }
186
+
187
+ function isNotUnicast(ip: ipaddr.IPv4 | ipaddr.IPv6): boolean {
188
+ return ip.range() !== 'unicast'
189
+ }
190
+
191
+ function parseLookupAddress({
192
+ address,
193
+ family,
194
+ }: LookupAddress): ipaddr.IPv4 | ipaddr.IPv6 {
195
+ const ip = family === 4 ? IPv4.parse(address) : IPv6.parse(address)
196
+
197
+ if (ip instanceof IPv6 && ip.isIPv4MappedAddress()) {
198
+ return ip.toIPv4Address()
199
+ } else {
200
+ return ip
201
+ }
202
+ }
package/src/util.ts ADDED
@@ -0,0 +1,22 @@
1
+ import ipaddr from 'ipaddr.js'
2
+
3
+ const { IPv4, IPv6 } = ipaddr
4
+
5
+ function parseIpHostname(
6
+ hostname: string,
7
+ ): ipaddr.IPv4 | ipaddr.IPv6 | undefined {
8
+ if (IPv4.isIPv4(hostname)) {
9
+ return IPv4.parse(hostname)
10
+ }
11
+
12
+ if (hostname.startsWith('[') && hostname.endsWith(']')) {
13
+ return IPv6.parse(hostname.slice(1, -1))
14
+ }
15
+
16
+ return undefined
17
+ }
18
+
19
+ export function isUnicastIp(hostname: string): boolean | undefined {
20
+ const ip = parseIpHostname(hostname)
21
+ return ip ? ip.range() === 'unicast' : undefined
22
+ }
package/dist/ssrf.d.ts DELETED
@@ -1,11 +0,0 @@
1
- import { Fetch, FetchContext } from '@atproto-labs/fetch';
2
- export type SsrfFetchWrapOptions<C = FetchContext> = {
3
- allowCustomPort?: boolean;
4
- allowUnknownTld?: boolean;
5
- fetch?: Fetch<C>;
6
- };
7
- /**
8
- * @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/}
9
- */
10
- export declare function ssrfFetchWrap<C = FetchContext>({ allowCustomPort, allowUnknownTld, fetch, }: SsrfFetchWrapOptions<C>): Fetch<C>;
11
- //# sourceMappingURL=ssrf.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ssrf.d.ts","sourceRoot":"","sources":["../src/ssrf.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,EACL,YAAY,EAGb,MAAM,qBAAqB,CAAA;AAS5B,MAAM,MAAM,oBAAoB,CAAC,CAAC,GAAG,YAAY,IAAI;IACnD,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,CAAA;CACjB,CAAA;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,CAAC,GAAG,YAAY,EAAE,EAC9C,eAAuB,EACvB,eAAuB,EACvB,KAAwB,GACzB,EAAE,oBAAoB,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAkIpC"}
package/dist/ssrf.js DELETED
@@ -1,138 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.ssrfFetchWrap = void 0;
7
- const node_dns_1 = __importDefault(require("node:dns"));
8
- const fetch_1 = require("@atproto-labs/fetch");
9
- const ipaddr_js_1 = __importDefault(require("ipaddr.js"));
10
- const psl_1 = require("psl");
11
- const undici_1 = require("undici");
12
- const { IPv4, IPv6 } = ipaddr_js_1.default;
13
- const [NODE_VERSION] = process.versions.node.split('.').map(Number);
14
- /**
15
- * @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/}
16
- */
17
- function ssrfFetchWrap({ allowCustomPort = false, allowUnknownTld = false, fetch = globalThis.fetch, }) {
18
- const ssrfAgent = new undici_1.Agent({ connect: { lookup } });
19
- return (0, fetch_1.toRequestTransformer)(async function (request) {
20
- const url = new URL(request.url);
21
- if (url.protocol === 'data:') {
22
- // No SSRF issue
23
- return fetch.call(this, request);
24
- }
25
- if (url.protocol === 'http:' || url.protocol === 'https:') {
26
- // @ts-expect-error non-standard option
27
- if (request.dispatcher) {
28
- throw new fetch_1.FetchRequestError(request, 500, 'SSRF protection cannot be used with a custom request dispatcher');
29
- }
30
- // Check port (OWASP)
31
- if (url.port && !allowCustomPort) {
32
- throw new fetch_1.FetchRequestError(request, 400, 'Request port must be omitted or standard when SSRF is enabled');
33
- }
34
- // Disable HTTP redirections (OWASP)
35
- if (request.redirect === 'follow') {
36
- throw new fetch_1.FetchRequestError(request, 500, 'Request redirect must be "error" or "manual" when SSRF is enabled');
37
- }
38
- // If the hostname is an IP address, it must be a unicast address.
39
- const ip = parseIpHostname(url.hostname);
40
- if (ip) {
41
- if (ip.range() !== 'unicast') {
42
- throw new fetch_1.FetchRequestError(request, 400, 'Hostname resolved to non-unicast address');
43
- }
44
- // No additional check required
45
- return fetch.call(this, request);
46
- }
47
- if (allowUnknownTld !== true && !(0, psl_1.isValid)(url.hostname)) {
48
- throw new fetch_1.FetchRequestError(request, 400, 'Hostname is not a public domain');
49
- }
50
- // Else hostname is a domain name, use DNS lookup to check if it resolves
51
- // to a unicast address
52
- if (NODE_VERSION < 21) {
53
- // Note: due to the issue nodejs/undici#2828 (fixed in undici >=6.7.0,
54
- // Node >=21), the "dispatcher" property of the request object will not
55
- // be used by fetch(). As a workaround, we pass the dispatcher as second
56
- // argument to fetch() here, and make sure it is used (which might not be
57
- // the case if a custom fetch() function is used).
58
- if (fetch === globalThis.fetch) {
59
- // If the global fetch function is used, we can pass the dispatcher
60
- // singleton directly to the fetch function as we know it will be
61
- // used.
62
- // @ts-expect-error non-standard option
63
- return fetch.call(this, request, { dispatcher: ssrfAgent });
64
- }
65
- let didLookup = false;
66
- const dispatcher = new undici_1.Agent({
67
- connect: {
68
- lookup(...args) {
69
- didLookup = true;
70
- lookup(...args);
71
- },
72
- },
73
- });
74
- try {
75
- // @ts-expect-error non-standard option
76
- return await fetch.call(this, request, { dispatcher });
77
- }
78
- finally {
79
- // Free resources (we cannot await here since the response was not
80
- // consumed yet).
81
- void dispatcher.close().catch((err) => {
82
- // No biggie, but let's still log it
83
- console.warn('Failed to close dispatcher', err);
84
- });
85
- if (!didLookup) {
86
- // If you encounter this error, either upgrade to Node.js >=21 or
87
- // make sure that the requestInit object is passed as second
88
- // argument to the global fetch function.
89
- // eslint-disable-next-line no-unsafe-finally
90
- throw new fetch_1.FetchRequestError(request, 500, 'Unable to enforce SSRF protection');
91
- }
92
- }
93
- }
94
- // @ts-expect-error non-standard option
95
- return fetch(new Request(request, { dispatcher: ssrfAgent }));
96
- }
97
- // blob: about: file: all should be rejected
98
- throw new fetch_1.FetchRequestError(request, 400, `Forbidden protocol "${url.protocol}"`);
99
- });
100
- }
101
- exports.ssrfFetchWrap = ssrfFetchWrap;
102
- function parseIpHostname(hostname) {
103
- if (IPv4.isIPv4(hostname)) {
104
- return IPv4.parse(hostname);
105
- }
106
- if (hostname.startsWith('[') && hostname.endsWith(']')) {
107
- return IPv6.parse(hostname.slice(1, -1));
108
- }
109
- return undefined;
110
- }
111
- function lookup(hostname, options, callback) {
112
- node_dns_1.default.lookup(hostname, options, (err, address, family) => {
113
- if (err) {
114
- callback(err, address, family);
115
- }
116
- else {
117
- const ips = Array.isArray(address)
118
- ? address.map(parseLookupAddress)
119
- : [parseLookupAddress({ address, family })];
120
- if (ips.some((ip) => ip.range() !== 'unicast')) {
121
- callback(new Error('Hostname resolved to non-unicast address'), address, family);
122
- }
123
- else {
124
- callback(null, address, family);
125
- }
126
- }
127
- });
128
- }
129
- function parseLookupAddress({ address, family, }) {
130
- const ip = family === 4 ? IPv4.parse(address) : IPv6.parse(address);
131
- if (ip instanceof IPv6 && ip.isIPv4MappedAddress()) {
132
- return ip.toIPv4Address();
133
- }
134
- else {
135
- return ip;
136
- }
137
- }
138
- //# sourceMappingURL=ssrf.js.map
package/dist/ssrf.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"ssrf.js","sourceRoot":"","sources":["../src/ssrf.ts"],"names":[],"mappings":"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"}
package/src/ssrf.ts DELETED
@@ -1,214 +0,0 @@
1
- import dns, { LookupAddress } from 'node:dns'
2
- import { LookupFunction } from 'node:net'
3
-
4
- import {
5
- Fetch,
6
- FetchContext,
7
- FetchRequestError,
8
- toRequestTransformer,
9
- } from '@atproto-labs/fetch'
10
- import ipaddr from 'ipaddr.js'
11
- import { isValid as isValidDomain } from 'psl'
12
- import { Agent } from 'undici'
13
-
14
- const { IPv4, IPv6 } = ipaddr
15
-
16
- const [NODE_VERSION] = process.versions.node.split('.').map(Number)
17
-
18
- export type SsrfFetchWrapOptions<C = FetchContext> = {
19
- allowCustomPort?: boolean
20
- allowUnknownTld?: boolean
21
- fetch?: Fetch<C>
22
- }
23
-
24
- /**
25
- * @see {@link https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/}
26
- */
27
- export function ssrfFetchWrap<C = FetchContext>({
28
- allowCustomPort = false,
29
- allowUnknownTld = false,
30
- fetch = globalThis.fetch,
31
- }: SsrfFetchWrapOptions<C>): Fetch<C> {
32
- const ssrfAgent = new Agent({ connect: { lookup } })
33
-
34
- return toRequestTransformer(async function (
35
- this: C,
36
- request,
37
- ): Promise<Response> {
38
- const url = new URL(request.url)
39
-
40
- if (url.protocol === 'data:') {
41
- // No SSRF issue
42
- return fetch.call(this, request)
43
- }
44
-
45
- if (url.protocol === 'http:' || url.protocol === 'https:') {
46
- // @ts-expect-error non-standard option
47
- if (request.dispatcher) {
48
- throw new FetchRequestError(
49
- request,
50
- 500,
51
- 'SSRF protection cannot be used with a custom request dispatcher',
52
- )
53
- }
54
-
55
- // Check port (OWASP)
56
- if (url.port && !allowCustomPort) {
57
- throw new FetchRequestError(
58
- request,
59
- 400,
60
- 'Request port must be omitted or standard when SSRF is enabled',
61
- )
62
- }
63
-
64
- // Disable HTTP redirections (OWASP)
65
- if (request.redirect === 'follow') {
66
- throw new FetchRequestError(
67
- request,
68
- 500,
69
- 'Request redirect must be "error" or "manual" when SSRF is enabled',
70
- )
71
- }
72
-
73
- // If the hostname is an IP address, it must be a unicast address.
74
- const ip = parseIpHostname(url.hostname)
75
- if (ip) {
76
- if (ip.range() !== 'unicast') {
77
- throw new FetchRequestError(
78
- request,
79
- 400,
80
- 'Hostname resolved to non-unicast address',
81
- )
82
- }
83
- // No additional check required
84
- return fetch.call(this, request)
85
- }
86
-
87
- if (allowUnknownTld !== true && !isValidDomain(url.hostname)) {
88
- throw new FetchRequestError(
89
- request,
90
- 400,
91
- 'Hostname is not a public domain',
92
- )
93
- }
94
-
95
- // Else hostname is a domain name, use DNS lookup to check if it resolves
96
- // to a unicast address
97
-
98
- if (NODE_VERSION < 21) {
99
- // Note: due to the issue nodejs/undici#2828 (fixed in undici >=6.7.0,
100
- // Node >=21), the "dispatcher" property of the request object will not
101
- // be used by fetch(). As a workaround, we pass the dispatcher as second
102
- // argument to fetch() here, and make sure it is used (which might not be
103
- // the case if a custom fetch() function is used).
104
-
105
- if (fetch === globalThis.fetch) {
106
- // If the global fetch function is used, we can pass the dispatcher
107
- // singleton directly to the fetch function as we know it will be
108
- // used.
109
-
110
- // @ts-expect-error non-standard option
111
- return fetch.call(this, request, { dispatcher: ssrfAgent })
112
- }
113
-
114
- let didLookup = false
115
- const dispatcher = new Agent({
116
- connect: {
117
- lookup(...args) {
118
- didLookup = true
119
- lookup(...args)
120
- },
121
- },
122
- })
123
-
124
- try {
125
- // @ts-expect-error non-standard option
126
- return await fetch.call(this, request, { dispatcher })
127
- } finally {
128
- // Free resources (we cannot await here since the response was not
129
- // consumed yet).
130
- void dispatcher.close().catch((err) => {
131
- // No biggie, but let's still log it
132
- console.warn('Failed to close dispatcher', err)
133
- })
134
-
135
- if (!didLookup) {
136
- // If you encounter this error, either upgrade to Node.js >=21 or
137
- // make sure that the requestInit object is passed as second
138
- // argument to the global fetch function.
139
-
140
- // eslint-disable-next-line no-unsafe-finally
141
- throw new FetchRequestError(
142
- request,
143
- 500,
144
- 'Unable to enforce SSRF protection',
145
- )
146
- }
147
- }
148
- }
149
-
150
- // @ts-expect-error non-standard option
151
- return fetch(new Request(request, { dispatcher: ssrfAgent }))
152
- }
153
-
154
- // blob: about: file: all should be rejected
155
- throw new FetchRequestError(
156
- request,
157
- 400,
158
- `Forbidden protocol "${url.protocol}"`,
159
- )
160
- })
161
- }
162
-
163
- function parseIpHostname(
164
- hostname: string,
165
- ): ipaddr.IPv4 | ipaddr.IPv6 | undefined {
166
- if (IPv4.isIPv4(hostname)) {
167
- return IPv4.parse(hostname)
168
- }
169
-
170
- if (hostname.startsWith('[') && hostname.endsWith(']')) {
171
- return IPv6.parse(hostname.slice(1, -1))
172
- }
173
-
174
- return undefined
175
- }
176
-
177
- function lookup(
178
- hostname: string,
179
- options: dns.LookupOptions,
180
- callback: Parameters<LookupFunction>[2],
181
- ) {
182
- dns.lookup(hostname, options, (err, address, family) => {
183
- if (err) {
184
- callback(err, address, family)
185
- } else {
186
- const ips = Array.isArray(address)
187
- ? address.map(parseLookupAddress)
188
- : [parseLookupAddress({ address, family })]
189
-
190
- if (ips.some((ip) => ip.range() !== 'unicast')) {
191
- callback(
192
- new Error('Hostname resolved to non-unicast address'),
193
- address,
194
- family,
195
- )
196
- } else {
197
- callback(null, address, family)
198
- }
199
- }
200
- })
201
- }
202
-
203
- function parseLookupAddress({
204
- address,
205
- family,
206
- }: LookupAddress): ipaddr.IPv4 | ipaddr.IPv6 {
207
- const ip = family === 4 ? IPv4.parse(address) : IPv6.parse(address)
208
-
209
- if (ip instanceof IPv6 && ip.isIPv4MappedAddress()) {
210
- return ip.toIPv4Address()
211
- } else {
212
- return ip
213
- }
214
- }