@atproto-labs/fetch-node 0.1.7 → 0.1.9
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 +23 -0
- package/LICENSE.txt +1 -1
- package/dist/safe.d.ts +10 -2
- package/dist/safe.d.ts.map +1 -1
- package/dist/safe.js +12 -5
- package/dist/safe.js.map +1 -1
- package/dist/unicast.d.ts +6 -0
- package/dist/unicast.d.ts.map +1 -1
- package/dist/unicast.js +37 -19
- package/dist/unicast.js.map +1 -1
- package/package.json +3 -5
- package/src/safe.ts +52 -48
- package/src/unicast.ts +41 -20
- package/tsconfig.build.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# @atproto-labs/fetch-node
|
|
2
2
|
|
|
3
|
+
## 0.1.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#3821](https://github.com/bluesky-social/atproto/pull/3821) [`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow explicit `follow` mode in safe fetch wrap.
|
|
8
|
+
|
|
9
|
+
- [#3819](https://github.com/bluesky-social/atproto/pull/3819) [`36dbd4155`](https://github.com/bluesky-social/atproto/commit/36dbd41551f74052a3f584719a1a7edd86eca201) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Fix potential memory leak
|
|
10
|
+
|
|
11
|
+
- [#3821](https://github.com/bluesky-social/atproto/pull/3821) [`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Allow disabling the need for an explicit `redirect` mode
|
|
12
|
+
|
|
13
|
+
- [#3818](https://github.com/bluesky-social/atproto/pull/3818) [`43861a452`](https://github.com/bluesky-social/atproto/commit/43861a452b70268e738ef12033297cddacbe25d4) Thanks [@matthieusieben](https://github.com/matthieusieben)! - Remove dependency on the Public Suffix List
|
|
14
|
+
|
|
15
|
+
- Updated dependencies [[`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4), [`5050b6550`](https://github.com/bluesky-social/atproto/commit/5050b6550e07e71b0a524eda0b71b837583294d4)]:
|
|
16
|
+
- @atproto-labs/pipe@0.1.1
|
|
17
|
+
- @atproto-labs/fetch@0.2.3
|
|
18
|
+
|
|
19
|
+
## 0.1.8
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- Updated dependencies [[`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29), [`850e39843`](https://github.com/bluesky-social/atproto/commit/850e39843cb0ec9ea716675f7568c0c601f45e29)]:
|
|
24
|
+
- @atproto-labs/fetch@0.2.2
|
|
25
|
+
|
|
3
26
|
## 0.1.7
|
|
4
27
|
|
|
5
28
|
### Patch Changes
|
package/LICENSE.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Dual MIT/Apache-2.0 License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2022-
|
|
3
|
+
Copyright (c) 2022-2025 Bluesky Social PBC, and Contributors
|
|
4
4
|
|
|
5
5
|
Except as otherwise noted in individual files, this software is licensed under the MIT license (<http://opensource.org/licenses/MIT>), or the Apache License, Version 2.0 (<http://www.apache.org/licenses/LICENSE-2.0>).
|
|
6
6
|
|
package/dist/safe.d.ts
CHANGED
|
@@ -7,7 +7,14 @@ export type SafeFetchWrapOptions = NonNullable<Parameters<typeof safeFetchWrap>[
|
|
|
7
7
|
* @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html}
|
|
8
8
|
*/
|
|
9
9
|
export declare function safeFetchWrap({ fetch, responseMaxSize, // 512kB
|
|
10
|
-
ssrfProtection, allowCustomPort, allowData, allowHttp, allowIpHost, allowPrivateIps, timeout, forbiddenDomainNames,
|
|
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
|
+
*
|
|
15
|
+
* @default false
|
|
16
|
+
*/
|
|
17
|
+
allowImplicitRedirect, }?: {
|
|
11
18
|
fetch?: Fetch | undefined;
|
|
12
19
|
responseMaxSize?: number | undefined;
|
|
13
20
|
ssrfProtection?: boolean | undefined;
|
|
@@ -18,5 +25,6 @@ ssrfProtection, allowCustomPort, allowData, allowHttp, allowIpHost, allowPrivate
|
|
|
18
25
|
allowPrivateIps?: boolean | undefined;
|
|
19
26
|
timeout?: number | undefined;
|
|
20
27
|
forbiddenDomainNames?: Iterable<string> | undefined;
|
|
21
|
-
|
|
28
|
+
allowImplicitRedirect?: boolean | undefined;
|
|
29
|
+
}): (input: string | URL | Request, init?: RequestInit | undefined) => Promise<Response>;
|
|
22
30
|
//# 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":"AAAA,OAAO,EAEL,KAAK,
|
|
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"}
|
package/dist/safe.js
CHANGED
|
@@ -11,12 +11,19 @@ const unicast_js_1 = require("./unicast.js");
|
|
|
11
11
|
* @see {@link https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html}
|
|
12
12
|
*/
|
|
13
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
|
-
|
|
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
|
+
*
|
|
19
|
+
* @default false
|
|
20
|
+
*/
|
|
21
|
+
allowImplicitRedirect = false, } = {}) {
|
|
22
|
+
return (0, pipe_1.pipe)(
|
|
16
23
|
/**
|
|
17
|
-
*
|
|
24
|
+
* Require explicit {@link RequestInit['redirect']} mode
|
|
18
25
|
*/
|
|
19
|
-
(0, fetch_1.
|
|
26
|
+
allowImplicitRedirect ? fetch_1.asRequest : (0, fetch_1.explicitRedirectCheckRequestTransform)(),
|
|
20
27
|
/**
|
|
21
28
|
* Only requests that will be issued with a "Host" header are allowed.
|
|
22
29
|
*/
|
|
@@ -53,6 +60,6 @@ ssrfProtection = true, allowCustomPort = !ssrfProtection, allowData = false, all
|
|
|
53
60
|
* Since we will be fetching user owned data, we need to make sure that an
|
|
54
61
|
* attacker cannot force us to download a large amounts of data.
|
|
55
62
|
*/
|
|
56
|
-
(0, fetch_1.fetchMaxSizeProcessor)(responseMaxSize))
|
|
63
|
+
(0, fetch_1.fetchMaxSizeProcessor)(responseMaxSize));
|
|
57
64
|
}
|
|
58
65
|
//# sourceMappingURL=safe.js.map
|
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":";;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"}
|
package/dist/unicast.d.ts
CHANGED
|
@@ -9,4 +9,10 @@ export type SsrfFetchWrapOptions<C = FetchContext> = {
|
|
|
9
9
|
*/
|
|
10
10
|
export declare function unicastFetchWrap<C = FetchContext>({ fetch, }: SsrfFetchWrapOptions<C>): Fetch<C>;
|
|
11
11
|
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;
|
|
12
18
|
//# 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;
|
|
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"}
|
package/dist/unicast.js
CHANGED
|
@@ -5,9 +5,9 @@ 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;
|
|
8
9
|
const node_dns_1 = __importDefault(require("node:dns"));
|
|
9
10
|
const ipaddr_js_1 = __importDefault(require("ipaddr.js"));
|
|
10
|
-
const psl_1 = require("psl");
|
|
11
11
|
const undici_1 = require("undici");
|
|
12
12
|
const fetch_1 = require("@atproto-labs/fetch");
|
|
13
13
|
const util_js_1 = require("./util.js");
|
|
@@ -77,15 +77,31 @@ function unicastFetchWrap({ fetch = globalThis.fetch, }) {
|
|
|
77
77
|
},
|
|
78
78
|
},
|
|
79
79
|
});
|
|
80
|
-
const headers = new Headers(init?.headers);
|
|
81
|
-
headers.set('connection', 'close'); // Proactively close the connection
|
|
82
80
|
try {
|
|
83
|
-
|
|
81
|
+
const headers = new Headers(init?.headers);
|
|
82
|
+
headers.set('connection', 'close'); // Proactively close the connection
|
|
83
|
+
const response = await fetch.call(this, input, {
|
|
84
84
|
...init,
|
|
85
85
|
headers,
|
|
86
86
|
// @ts-expect-error non-standard option
|
|
87
87
|
dispatcher,
|
|
88
88
|
});
|
|
89
|
+
if (!didLookup) {
|
|
90
|
+
// We need to ensure that the body is discarded. We can either
|
|
91
|
+
// consume the whole body (for await loop) in order to keep the
|
|
92
|
+
// socket alive, or cancel the request. Since we sent "connection:
|
|
93
|
+
// close", there is no point in consuming the whole response
|
|
94
|
+
// (which would cause un-necessary bandwidth).
|
|
95
|
+
//
|
|
96
|
+
// https://undici.nodejs.org/#/?id=garbage-collection
|
|
97
|
+
await response.body?.cancel();
|
|
98
|
+
// If you encounter this error, either upgrade to Node.js >=21 or
|
|
99
|
+
// make sure that the dispatcher passed through the requestInit
|
|
100
|
+
// object ends up being used to make the request.
|
|
101
|
+
// eslint-disable-next-line no-unsafe-finally
|
|
102
|
+
throw new fetch_1.FetchRequestError((0, fetch_1.asRequest)(input, init), 500, 'Unable to enforce SSRF protection');
|
|
103
|
+
}
|
|
104
|
+
return response;
|
|
89
105
|
}
|
|
90
106
|
finally {
|
|
91
107
|
// Free resources (we cannot await here since the response was not
|
|
@@ -94,13 +110,6 @@ function unicastFetchWrap({ fetch = globalThis.fetch, }) {
|
|
|
94
110
|
// No biggie, but let's still log it
|
|
95
111
|
console.warn('Failed to close dispatcher', err);
|
|
96
112
|
});
|
|
97
|
-
if (!didLookup) {
|
|
98
|
-
// If you encounter this error, either upgrade to Node.js >=21 or
|
|
99
|
-
// make sure that the dispatcher passed through the requestInit
|
|
100
|
-
// object ends up being used to make the request.
|
|
101
|
-
// eslint-disable-next-line no-unsafe-finally
|
|
102
|
-
throw new fetch_1.FetchRequestError((0, fetch_1.asRequest)(input, init), 500, 'Unable to enforce SSRF protection');
|
|
103
|
-
}
|
|
104
113
|
}
|
|
105
114
|
}
|
|
106
115
|
}
|
|
@@ -108,8 +117,8 @@ function unicastFetchWrap({ fetch = globalThis.fetch, }) {
|
|
|
108
117
|
}
|
|
109
118
|
}
|
|
110
119
|
function unicastLookup(hostname, options, callback) {
|
|
111
|
-
if (
|
|
112
|
-
callback(new Error('Hostname is not a public domain'),
|
|
120
|
+
if (isLocalHostname(hostname)) {
|
|
121
|
+
callback(new Error('Hostname is not a public domain'), []);
|
|
113
122
|
return;
|
|
114
123
|
}
|
|
115
124
|
node_dns_1.default.lookup(hostname, options, (err, address, family) => {
|
|
@@ -129,12 +138,21 @@ function unicastLookup(hostname, options, callback) {
|
|
|
129
138
|
}
|
|
130
139
|
});
|
|
131
140
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
141
|
+
/**
|
|
142
|
+
* @param hostname - a syntactically valid hostname
|
|
143
|
+
* @returns whether the hostname is a name typically used for on locale area networks.
|
|
144
|
+
* @note **DO NOT** use for security reasons. Only as heuristic.
|
|
145
|
+
*/
|
|
146
|
+
function isLocalHostname(hostname) {
|
|
147
|
+
const parts = hostname.split('.');
|
|
148
|
+
if (parts.length < 2)
|
|
149
|
+
return true;
|
|
150
|
+
const tld = parts.at(-1).toLowerCase();
|
|
151
|
+
return (tld === 'test' ||
|
|
152
|
+
tld === 'local' ||
|
|
153
|
+
tld === 'localhost' ||
|
|
154
|
+
tld === 'invalid' ||
|
|
155
|
+
tld === 'example');
|
|
138
156
|
}
|
|
139
157
|
function isNotUnicast(ip) {
|
|
140
158
|
return ip.range() !== 'unicast';
|
package/dist/unicast.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"unicast.js","sourceRoot":"","sources":["../src/unicast.ts"],"names":[],"mappings":";;;;;
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto-labs/fetch-node",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "SSRF protection for fetch() in Node.js",
|
|
6
6
|
"keywords": [
|
|
@@ -28,13 +28,11 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"ipaddr.js": "^2.1.0",
|
|
31
|
-
"psl": "^1.9.0",
|
|
32
31
|
"undici": "^6.14.1",
|
|
33
|
-
"@atproto-labs/fetch": "0.2.
|
|
34
|
-
"@atproto-labs/pipe": "0.1.
|
|
32
|
+
"@atproto-labs/fetch": "0.2.3",
|
|
33
|
+
"@atproto-labs/pipe": "0.1.1"
|
|
35
34
|
},
|
|
36
35
|
"devDependencies": {
|
|
37
|
-
"@types/psl": "1.1.3",
|
|
38
36
|
"typescript": "^5.6.3"
|
|
39
37
|
},
|
|
40
38
|
"scripts": {
|
package/src/safe.ts
CHANGED
|
@@ -2,13 +2,12 @@ import {
|
|
|
2
2
|
DEFAULT_FORBIDDEN_DOMAIN_NAMES,
|
|
3
3
|
Fetch,
|
|
4
4
|
asRequest,
|
|
5
|
+
explicitRedirectCheckRequestTransform,
|
|
5
6
|
fetchMaxSizeProcessor,
|
|
6
7
|
forbiddenDomainNameRequestTransform,
|
|
7
8
|
protocolCheckRequestTransform,
|
|
8
|
-
redirectCheckRequestTransform,
|
|
9
9
|
requireHostHeaderTransform,
|
|
10
10
|
timedFetch,
|
|
11
|
-
toRequestTransformer,
|
|
12
11
|
} from '@atproto-labs/fetch'
|
|
13
12
|
import { pipe } from '@atproto-labs/pipe'
|
|
14
13
|
import { unicastFetchWrap } from './unicast.js'
|
|
@@ -34,58 +33,63 @@ export function safeFetchWrap({
|
|
|
34
33
|
allowPrivateIps = !ssrfProtection,
|
|
35
34
|
timeout = 10e3,
|
|
36
35
|
forbiddenDomainNames = DEFAULT_FORBIDDEN_DOMAIN_NAMES as Iterable<string>,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
allowImplicitRedirect = false,
|
|
43
|
+
} = {}) {
|
|
44
|
+
return pipe(
|
|
45
|
+
/**
|
|
46
|
+
* Require explicit {@link RequestInit['redirect']} mode
|
|
47
|
+
*/
|
|
48
|
+
allowImplicitRedirect ? asRequest : explicitRedirectCheckRequestTransform(),
|
|
44
49
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Prevent using http:, file: or data: protocols.
|
|
52
|
-
*/
|
|
53
|
-
protocolCheckRequestTransform({
|
|
54
|
-
'about:': false,
|
|
55
|
-
'data:': allowData,
|
|
56
|
-
'file:': false,
|
|
57
|
-
'http:': allowHttp && { allowCustomPort },
|
|
58
|
-
'https:': { allowCustomPort },
|
|
59
|
-
}),
|
|
50
|
+
/**
|
|
51
|
+
* Only requests that will be issued with a "Host" header are allowed.
|
|
52
|
+
*/
|
|
53
|
+
allowIpHost ? asRequest : requireHostHeaderTransform(),
|
|
60
54
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Prevent using http:, file: or data: protocols.
|
|
57
|
+
*/
|
|
58
|
+
protocolCheckRequestTransform({
|
|
59
|
+
'about:': false,
|
|
60
|
+
'data:': allowData,
|
|
61
|
+
'file:': false,
|
|
62
|
+
'http:': allowHttp && { allowCustomPort },
|
|
63
|
+
'https:': { allowCustomPort },
|
|
64
|
+
}),
|
|
68
65
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
/**
|
|
67
|
+
* Disallow fetching from domains we know are not atproto/OIDC client
|
|
68
|
+
* implementation. Note that other domains can be blocked by providing a
|
|
69
|
+
* custom fetch function combined with another
|
|
70
|
+
* forbiddenDomainNameRequestTransform.
|
|
71
|
+
*/
|
|
72
|
+
forbiddenDomainNameRequestTransform(forbiddenDomainNames),
|
|
75
73
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
),
|
|
74
|
+
/**
|
|
75
|
+
* Since we will be fetching from the network based on user provided
|
|
76
|
+
* input, let's mitigate resource exhaustion attacks by setting a timeout.
|
|
77
|
+
*/
|
|
78
|
+
timedFetch(
|
|
79
|
+
timeout,
|
|
83
80
|
|
|
84
81
|
/**
|
|
85
|
-
* Since we will be fetching
|
|
86
|
-
*
|
|
82
|
+
* Since we will be fetching from the network based on user provided
|
|
83
|
+
* input, we need to make sure that the request is not vulnerable to SSRF
|
|
84
|
+
* attacks.
|
|
87
85
|
*/
|
|
88
|
-
|
|
86
|
+
allowPrivateIps ? fetch : unicastFetchWrap({ fetch }),
|
|
89
87
|
),
|
|
90
|
-
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Since we will be fetching user owned data, we need to make sure that an
|
|
91
|
+
* attacker cannot force us to download a large amounts of data.
|
|
92
|
+
*/
|
|
93
|
+
fetchMaxSizeProcessor(responseMaxSize),
|
|
94
|
+
) satisfies Fetch<unknown>
|
|
91
95
|
}
|
package/src/unicast.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import dns, { LookupAddress } from 'node:dns'
|
|
2
2
|
import { LookupFunction } from 'node:net'
|
|
3
3
|
import ipaddr from 'ipaddr.js'
|
|
4
|
-
import { parse as pslParse } from 'psl'
|
|
5
4
|
import { Agent, Client } from 'undici'
|
|
6
5
|
import {
|
|
7
6
|
Fetch,
|
|
@@ -115,25 +114,27 @@ export function unicastFetchWrap<C = FetchContext>({
|
|
|
115
114
|
},
|
|
116
115
|
})
|
|
117
116
|
|
|
118
|
-
const headers = new Headers(init?.headers)
|
|
119
|
-
headers.set('connection', 'close') // Proactively close the connection
|
|
120
|
-
|
|
121
117
|
try {
|
|
122
|
-
|
|
118
|
+
const headers = new Headers(init?.headers)
|
|
119
|
+
headers.set('connection', 'close') // Proactively close the connection
|
|
120
|
+
|
|
121
|
+
const response = await fetch.call(this, input, {
|
|
123
122
|
...init,
|
|
124
123
|
headers,
|
|
125
124
|
// @ts-expect-error non-standard option
|
|
126
125
|
dispatcher,
|
|
127
126
|
})
|
|
128
|
-
} finally {
|
|
129
|
-
// Free resources (we cannot await here since the response was not
|
|
130
|
-
// consumed yet).
|
|
131
|
-
void dispatcher.close().catch((err) => {
|
|
132
|
-
// No biggie, but let's still log it
|
|
133
|
-
console.warn('Failed to close dispatcher', err)
|
|
134
|
-
})
|
|
135
127
|
|
|
136
128
|
if (!didLookup) {
|
|
129
|
+
// We need to ensure that the body is discarded. We can either
|
|
130
|
+
// consume the whole body (for await loop) in order to keep the
|
|
131
|
+
// socket alive, or cancel the request. Since we sent "connection:
|
|
132
|
+
// close", there is no point in consuming the whole response
|
|
133
|
+
// (which would cause un-necessary bandwidth).
|
|
134
|
+
//
|
|
135
|
+
// https://undici.nodejs.org/#/?id=garbage-collection
|
|
136
|
+
await response.body?.cancel()
|
|
137
|
+
|
|
137
138
|
// If you encounter this error, either upgrade to Node.js >=21 or
|
|
138
139
|
// make sure that the dispatcher passed through the requestInit
|
|
139
140
|
// object ends up being used to make the request.
|
|
@@ -145,6 +146,15 @@ export function unicastFetchWrap<C = FetchContext>({
|
|
|
145
146
|
'Unable to enforce SSRF protection',
|
|
146
147
|
)
|
|
147
148
|
}
|
|
149
|
+
|
|
150
|
+
return response
|
|
151
|
+
} finally {
|
|
152
|
+
// Free resources (we cannot await here since the response was not
|
|
153
|
+
// consumed yet).
|
|
154
|
+
void dispatcher.close().catch((err) => {
|
|
155
|
+
// No biggie, but let's still log it
|
|
156
|
+
console.warn('Failed to close dispatcher', err)
|
|
157
|
+
})
|
|
148
158
|
}
|
|
149
159
|
}
|
|
150
160
|
}
|
|
@@ -157,8 +167,8 @@ export function unicastLookup(
|
|
|
157
167
|
options: dns.LookupOptions,
|
|
158
168
|
callback: Parameters<LookupFunction>[2],
|
|
159
169
|
) {
|
|
160
|
-
if (
|
|
161
|
-
callback(new Error('Hostname is not a public domain'),
|
|
170
|
+
if (isLocalHostname(hostname)) {
|
|
171
|
+
callback(new Error('Hostname is not a public domain'), [])
|
|
162
172
|
return
|
|
163
173
|
}
|
|
164
174
|
|
|
@@ -183,12 +193,23 @@ export function unicastLookup(
|
|
|
183
193
|
})
|
|
184
194
|
}
|
|
185
195
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
196
|
+
/**
|
|
197
|
+
* @param hostname - a syntactically valid hostname
|
|
198
|
+
* @returns whether the hostname is a name typically used for on locale area networks.
|
|
199
|
+
* @note **DO NOT** use for security reasons. Only as heuristic.
|
|
200
|
+
*/
|
|
201
|
+
export function isLocalHostname(hostname: string): boolean {
|
|
202
|
+
const parts = hostname.split('.')
|
|
203
|
+
if (parts.length < 2) return true
|
|
204
|
+
|
|
205
|
+
const tld = parts.at(-1)!.toLowerCase()
|
|
206
|
+
return (
|
|
207
|
+
tld === 'test' ||
|
|
208
|
+
tld === 'local' ||
|
|
209
|
+
tld === 'localhost' ||
|
|
210
|
+
tld === 'invalid' ||
|
|
211
|
+
tld === 'example'
|
|
212
|
+
)
|
|
192
213
|
}
|
|
193
214
|
|
|
194
215
|
function isNotUnicast(ip: ipaddr.IPv4 | ipaddr.IPv6): boolean {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/index.ts","./src/safe.ts","./src/unicast.ts","./src/util.ts"],"version":"5.
|
|
1
|
+
{"root":["./src/index.ts","./src/safe.ts","./src/unicast.ts","./src/util.ts"],"version":"5.8.2"}
|