@ai-sdk/provider-utils 5.0.14 → 5.0.15
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 +7 -0
- package/dist/index.d.ts +8 -11
- package/dist/index.js +153 -41
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/fetch-with-validated-redirects.ts +15 -12
- package/src/get-from-api.ts +4 -2
- package/src/safe-node-fetch.ts +204 -0
- package/src/validate-download-url.ts +30 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# @ai-sdk/provider-utils
|
|
2
2
|
|
|
3
|
+
## 5.0.15
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 1659cd5: Prevent validated downloads on Node.js from reaching private or internal services through DNS aliases or DNS rebinding by validating and pinning every resolved address at connection time.
|
|
8
|
+
- 6a5bdff: Fix validated Node.js downloads when the HTTP connector requests a single DNS address.
|
|
9
|
+
|
|
3
10
|
## 5.0.14
|
|
4
11
|
|
|
5
12
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -856,18 +856,16 @@ type FetchFunction = typeof globalThis.fetch;
|
|
|
856
856
|
* The returned response is the final (non-redirect) response. The caller is
|
|
857
857
|
* responsible for checking `response.ok` and reading the body.
|
|
858
858
|
*
|
|
859
|
-
*
|
|
860
|
-
*
|
|
861
|
-
*
|
|
862
|
-
*
|
|
863
|
-
*
|
|
864
|
-
* time — those need DNS/socket APIs not available on all target runtimes
|
|
865
|
-
* (edge, browser, Bun), so they are intentionally not built in.
|
|
859
|
+
* On Node.js, the default fetch resolves every hostname through a validating
|
|
860
|
+
* lookup hook and passes those exact addresses to the connector, preventing
|
|
861
|
+
* hostname-to-private-IP and DNS-rebinding bypasses. An injected fetch is
|
|
862
|
+
* responsible for equivalent connect-time validation. Other runtimes should
|
|
863
|
+
* constrain egress at the network layer when handling untrusted URLs.
|
|
866
864
|
*
|
|
867
865
|
* @throws DownloadError if a hop is unsafe, the redirect limit is exceeded, or
|
|
868
866
|
* a redirect cannot be validated on a non-browser runtime.
|
|
869
867
|
*/
|
|
870
|
-
declare function fetchWithValidatedRedirects({ url, headers, abortSignal, maxRedirects, fetch, trustedOrigin, }: {
|
|
868
|
+
declare function fetchWithValidatedRedirects({ url, headers, abortSignal, maxRedirects, fetch: customFetch, trustedOrigin, }: {
|
|
871
869
|
url: string;
|
|
872
870
|
headers?: HeadersInit;
|
|
873
871
|
abortSignal?: AbortSignal;
|
|
@@ -2423,9 +2421,8 @@ declare function validateBaseURL(baseURL: string | undefined): string | undefine
|
|
|
2423
2421
|
* Validates that a URL is safe to download from, blocking private/internal addresses
|
|
2424
2422
|
* to prevent SSRF attacks.
|
|
2425
2423
|
*
|
|
2426
|
-
* Note: this performs string/literal-IP checks only.
|
|
2427
|
-
*
|
|
2428
|
-
* should additionally constrain egress at the network layer when handling untrusted URLs).
|
|
2424
|
+
* Note: this function performs string/literal-IP checks only. The Node.js
|
|
2425
|
+
* download fetch additionally validates and pins DNS results at connect time.
|
|
2429
2426
|
*
|
|
2430
2427
|
* @param url - The URL string to validate.
|
|
2431
2428
|
* @throws DownloadError if the URL is unsafe.
|
package/dist/index.js
CHANGED
|
@@ -686,42 +686,6 @@ function isSameOrigin(url, baseUrl) {
|
|
|
686
686
|
}
|
|
687
687
|
}
|
|
688
688
|
|
|
689
|
-
// src/sanitize-request-headers.ts
|
|
690
|
-
var BLOCKED_REQUEST_HEADERS = [
|
|
691
|
-
// Hop-by-hop / transport (RFC 7230 §6.1)
|
|
692
|
-
"connection",
|
|
693
|
-
"keep-alive",
|
|
694
|
-
"te",
|
|
695
|
-
"trailer",
|
|
696
|
-
"transfer-encoding",
|
|
697
|
-
"upgrade",
|
|
698
|
-
// Host / virtual-host routing
|
|
699
|
-
"host",
|
|
700
|
-
// Proxy / origin spoofing
|
|
701
|
-
"forwarded",
|
|
702
|
-
"proxy-authorization",
|
|
703
|
-
"via",
|
|
704
|
-
"x-forwarded-for",
|
|
705
|
-
"x-forwarded-host",
|
|
706
|
-
"x-forwarded-proto",
|
|
707
|
-
"x-real-ip",
|
|
708
|
-
// Cloud metadata (GCP, AWS IMDSv1/v2, Azure, Alibaba, DigitalOcean)
|
|
709
|
-
"metadata",
|
|
710
|
-
"metadata-flavor",
|
|
711
|
-
"x-aws-ec2-metadata-token",
|
|
712
|
-
"x-metadata-token",
|
|
713
|
-
// Session / cookie
|
|
714
|
-
"cookie",
|
|
715
|
-
"set-cookie"
|
|
716
|
-
];
|
|
717
|
-
function sanitizeRequestHeaders(input) {
|
|
718
|
-
const headers = new Headers(input);
|
|
719
|
-
for (const name3 of BLOCKED_REQUEST_HEADERS) {
|
|
720
|
-
headers.delete(name3);
|
|
721
|
-
}
|
|
722
|
-
return headers;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
689
|
// src/validate-download-url.ts
|
|
726
690
|
function validateDownloadUrl(url) {
|
|
727
691
|
let parsed;
|
|
@@ -775,6 +739,19 @@ function validateDownloadUrl(url) {
|
|
|
775
739
|
return;
|
|
776
740
|
}
|
|
777
741
|
}
|
|
742
|
+
function validateDownloadAddress({
|
|
743
|
+
address,
|
|
744
|
+
family,
|
|
745
|
+
hostname
|
|
746
|
+
}) {
|
|
747
|
+
const isUnsafe = family === 4 ? !isIPv4(address) || isPrivateIPv4(address) : family === 6 ? isPrivateIPv6(address) : true;
|
|
748
|
+
if (isUnsafe) {
|
|
749
|
+
throw new DownloadError({
|
|
750
|
+
url: hostname,
|
|
751
|
+
message: `Hostname ${hostname} resolved to disallowed IP address ${address}`
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
}
|
|
778
755
|
function isIPv4(hostname) {
|
|
779
756
|
const parts = hostname.split(".");
|
|
780
757
|
if (parts.length !== 4) return false;
|
|
@@ -866,6 +843,138 @@ function isPrivateIPv6(ip) {
|
|
|
866
843
|
return false;
|
|
867
844
|
}
|
|
868
845
|
|
|
846
|
+
// src/safe-node-fetch.ts
|
|
847
|
+
function createSafeLookup(lookup) {
|
|
848
|
+
return ((hostname, options, callback) => {
|
|
849
|
+
lookup(hostname, { ...options, all: true }, (error, addresses) => {
|
|
850
|
+
if (error) {
|
|
851
|
+
callback(error);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
try {
|
|
855
|
+
const [firstAddress] = addresses;
|
|
856
|
+
if (firstAddress == null) {
|
|
857
|
+
throw new Error(`Hostname ${hostname} did not resolve to an address`);
|
|
858
|
+
}
|
|
859
|
+
for (const { address, family } of addresses) {
|
|
860
|
+
validateDownloadAddress({ address, family, hostname });
|
|
861
|
+
}
|
|
862
|
+
if (options.all === true) {
|
|
863
|
+
callback(null, addresses);
|
|
864
|
+
} else {
|
|
865
|
+
callback(
|
|
866
|
+
null,
|
|
867
|
+
firstAddress.address,
|
|
868
|
+
firstAddress.family
|
|
869
|
+
);
|
|
870
|
+
}
|
|
871
|
+
} catch (error2) {
|
|
872
|
+
callback(
|
|
873
|
+
error2 instanceof Error ? error2 : new Error(String(error2))
|
|
874
|
+
);
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
var safeNodeFetchPromise;
|
|
880
|
+
var initialGlobalFetch = globalThis.fetch;
|
|
881
|
+
var initialGlobalFetchIsNodeDefault = isNodeDefaultFetch(initialGlobalFetch);
|
|
882
|
+
function isNodeRuntime() {
|
|
883
|
+
var _a3, _b3;
|
|
884
|
+
const runtimeProcess = globalThis.process;
|
|
885
|
+
return ((_a3 = runtimeProcess == null ? void 0 : runtimeProcess.release) == null ? void 0 : _a3.name) === "node" && ((_b3 = runtimeProcess.versions) == null ? void 0 : _b3.bun) == null;
|
|
886
|
+
}
|
|
887
|
+
async function getDefaultDownloadFetch() {
|
|
888
|
+
if (!isNodeRuntime() || !initialGlobalFetchIsNodeDefault || globalThis.fetch !== initialGlobalFetch) {
|
|
889
|
+
return globalThis.fetch;
|
|
890
|
+
}
|
|
891
|
+
return safeNodeFetchPromise != null ? safeNodeFetchPromise : safeNodeFetchPromise = Promise.resolve().then(createSafeNodeFetch);
|
|
892
|
+
}
|
|
893
|
+
function isNodeDefaultFetch(fetch) {
|
|
894
|
+
const source = Function.prototype.toString.call(fetch);
|
|
895
|
+
return source.includes("internal/deps/undici") || source.includes("lazy loading of undici");
|
|
896
|
+
}
|
|
897
|
+
function createSafeNodeFetch() {
|
|
898
|
+
const { createRequire } = loadBuiltinModule("node:module");
|
|
899
|
+
const { lookup } = loadBuiltinModule("node:dns");
|
|
900
|
+
const { Agent, fetch } = createRequire(getCurrentModulePath())(
|
|
901
|
+
"undici"
|
|
902
|
+
);
|
|
903
|
+
const dispatcher = new Agent({
|
|
904
|
+
connect: {
|
|
905
|
+
lookup: createSafeLookup(lookup)
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
return ((input, init) => fetch(
|
|
909
|
+
input,
|
|
910
|
+
{
|
|
911
|
+
...init,
|
|
912
|
+
dispatcher
|
|
913
|
+
}
|
|
914
|
+
));
|
|
915
|
+
}
|
|
916
|
+
function loadBuiltinModule(id) {
|
|
917
|
+
var _a3;
|
|
918
|
+
const processWithBuiltins = globalThis.process;
|
|
919
|
+
const builtinModule = (_a3 = processWithBuiltins == null ? void 0 : processWithBuiltins.getBuiltinModule) == null ? void 0 : _a3.call(processWithBuiltins, id);
|
|
920
|
+
if (builtinModule == null) {
|
|
921
|
+
throw new Error(`Node.js built-in module ${id} is unavailable`);
|
|
922
|
+
}
|
|
923
|
+
return builtinModule;
|
|
924
|
+
}
|
|
925
|
+
function getCurrentModulePath() {
|
|
926
|
+
const originalPrepareStackTrace = Error.prepareStackTrace;
|
|
927
|
+
try {
|
|
928
|
+
Error.prepareStackTrace = (_error, callSites) => callSites;
|
|
929
|
+
const error = new Error("Capture current module path");
|
|
930
|
+
Error.captureStackTrace(error, getCurrentModulePath);
|
|
931
|
+
const [caller] = error.stack;
|
|
932
|
+
const fileName = caller == null ? void 0 : caller.getFileName();
|
|
933
|
+
if (fileName == null) {
|
|
934
|
+
throw new Error("Unable to determine the current module path");
|
|
935
|
+
}
|
|
936
|
+
return fileName;
|
|
937
|
+
} finally {
|
|
938
|
+
Error.prepareStackTrace = originalPrepareStackTrace;
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// src/sanitize-request-headers.ts
|
|
943
|
+
var BLOCKED_REQUEST_HEADERS = [
|
|
944
|
+
// Hop-by-hop / transport (RFC 7230 §6.1)
|
|
945
|
+
"connection",
|
|
946
|
+
"keep-alive",
|
|
947
|
+
"te",
|
|
948
|
+
"trailer",
|
|
949
|
+
"transfer-encoding",
|
|
950
|
+
"upgrade",
|
|
951
|
+
// Host / virtual-host routing
|
|
952
|
+
"host",
|
|
953
|
+
// Proxy / origin spoofing
|
|
954
|
+
"forwarded",
|
|
955
|
+
"proxy-authorization",
|
|
956
|
+
"via",
|
|
957
|
+
"x-forwarded-for",
|
|
958
|
+
"x-forwarded-host",
|
|
959
|
+
"x-forwarded-proto",
|
|
960
|
+
"x-real-ip",
|
|
961
|
+
// Cloud metadata (GCP, AWS IMDSv1/v2, Azure, Alibaba, DigitalOcean)
|
|
962
|
+
"metadata",
|
|
963
|
+
"metadata-flavor",
|
|
964
|
+
"x-aws-ec2-metadata-token",
|
|
965
|
+
"x-metadata-token",
|
|
966
|
+
// Session / cookie
|
|
967
|
+
"cookie",
|
|
968
|
+
"set-cookie"
|
|
969
|
+
];
|
|
970
|
+
function sanitizeRequestHeaders(input) {
|
|
971
|
+
const headers = new Headers(input);
|
|
972
|
+
for (const name3 of BLOCKED_REQUEST_HEADERS) {
|
|
973
|
+
headers.delete(name3);
|
|
974
|
+
}
|
|
975
|
+
return headers;
|
|
976
|
+
}
|
|
977
|
+
|
|
869
978
|
// src/fetch-with-validated-redirects.ts
|
|
870
979
|
var MAX_DOWNLOAD_REDIRECTS = 10;
|
|
871
980
|
var REDIRECT_STATUS_CODES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
|
|
@@ -874,7 +983,7 @@ async function fetchWithValidatedRedirects({
|
|
|
874
983
|
headers,
|
|
875
984
|
abortSignal,
|
|
876
985
|
maxRedirects = MAX_DOWNLOAD_REDIRECTS,
|
|
877
|
-
fetch
|
|
986
|
+
fetch: customFetch,
|
|
878
987
|
trustedOrigin
|
|
879
988
|
}) {
|
|
880
989
|
let currentHeaders = headers === void 0 ? void 0 : sanitizeRequestHeaders(headers);
|
|
@@ -887,9 +996,11 @@ async function fetchWithValidatedRedirects({
|
|
|
887
996
|
};
|
|
888
997
|
let currentUrl = url;
|
|
889
998
|
for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount++) {
|
|
890
|
-
|
|
999
|
+
const isTrustedHop = trustedOrigin !== void 0 && isSameOrigin(currentUrl, trustedOrigin);
|
|
1000
|
+
if (!isTrustedHop) {
|
|
891
1001
|
validateDownloadUrl(currentUrl);
|
|
892
1002
|
}
|
|
1003
|
+
const fetch = customFetch != null ? customFetch : isTrustedHop ? globalThis.fetch : await getDefaultDownloadFetch();
|
|
893
1004
|
const response = await fetch(currentUrl, perHopInit("manual"));
|
|
894
1005
|
if (response.type === "opaqueredirect") {
|
|
895
1006
|
if (!isBrowserRuntime()) {
|
|
@@ -1183,7 +1294,7 @@ function withUserAgentSuffix(headers, ...userAgentSuffixParts) {
|
|
|
1183
1294
|
}
|
|
1184
1295
|
|
|
1185
1296
|
// src/version.ts
|
|
1186
|
-
var VERSION = true ? "5.0.
|
|
1297
|
+
var VERSION = true ? "5.0.15" : "0.0.0-test";
|
|
1187
1298
|
|
|
1188
1299
|
// src/get-from-api.ts
|
|
1189
1300
|
var getOriginalFetch = () => globalThis.fetch;
|
|
@@ -1193,12 +1304,13 @@ var getFromApi = async ({
|
|
|
1193
1304
|
successfulResponseHandler,
|
|
1194
1305
|
failedResponseHandler,
|
|
1195
1306
|
abortSignal,
|
|
1196
|
-
fetch
|
|
1307
|
+
fetch,
|
|
1197
1308
|
validateUrl,
|
|
1198
1309
|
credentialedOrigin,
|
|
1199
1310
|
trustedOrigin
|
|
1200
1311
|
}) => {
|
|
1201
1312
|
try {
|
|
1313
|
+
const requestFetch = fetch != null ? fetch : getOriginalFetch();
|
|
1202
1314
|
const outgoingHeaders = credentialedOrigin !== void 0 && !isSameOrigin(url, credentialedOrigin) ? {} : headers;
|
|
1203
1315
|
const requestHeaders = withUserAgentSuffix(
|
|
1204
1316
|
outgoingHeaders,
|
|
@@ -1211,7 +1323,7 @@ var getFromApi = async ({
|
|
|
1211
1323
|
abortSignal,
|
|
1212
1324
|
fetch,
|
|
1213
1325
|
trustedOrigin
|
|
1214
|
-
}) : await
|
|
1326
|
+
}) : await requestFetch(url, {
|
|
1215
1327
|
method: "GET",
|
|
1216
1328
|
headers: requestHeaders,
|
|
1217
1329
|
signal: abortSignal
|