@ai-sdk/provider-utils 3.0.30 → 3.0.31

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,11 @@
1
1
  # @ai-sdk/provider-utils
2
2
 
3
+ ## 3.0.31
4
+
5
+ ### Patch Changes
6
+
7
+ - 7a6bdbc: 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
+
3
9
  ## 3.0.30
4
10
 
5
11
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -92,6 +92,11 @@ declare class DownloadError extends AISDKError {
92
92
  * The returned response is the final (non-redirect) response. The caller is
93
93
  * responsible for checking `response.ok` and reading the body.
94
94
  *
95
+ * On Node.js, the default fetch resolves every hostname through a validating
96
+ * lookup hook and passes those exact addresses to the connector, preventing
97
+ * hostname-to-private-IP and DNS-rebinding bypasses. Other runtimes should
98
+ * constrain egress at the network layer when handling untrusted URLs.
99
+ *
95
100
  * @throws DownloadError if a hop is unsafe, the redirect limit is exceeded, or
96
101
  * a redirect cannot be validated on a non-browser runtime.
97
102
  */
@@ -945,9 +950,8 @@ declare function convertToBase64(value: string | Uint8Array): string;
945
950
  * Validates that a URL is safe to download from, blocking private/internal addresses
946
951
  * to prevent SSRF attacks.
947
952
  *
948
- * Note: this performs string/literal-IP checks only. It does not resolve DNS, so a
949
- * hostname that resolves to a private address is not blocked here (see callers, which
950
- * should additionally constrain egress at the network layer when handling untrusted URLs).
953
+ * Note: this function performs string/literal-IP checks only. The Node.js
954
+ * download fetch additionally validates and pins DNS results at connect time.
951
955
  *
952
956
  * @param url - The URL string to validate.
953
957
  * @throws DownloadError if the URL is unsafe.
package/dist/index.d.ts CHANGED
@@ -92,6 +92,11 @@ declare class DownloadError extends AISDKError {
92
92
  * The returned response is the final (non-redirect) response. The caller is
93
93
  * responsible for checking `response.ok` and reading the body.
94
94
  *
95
+ * On Node.js, the default fetch resolves every hostname through a validating
96
+ * lookup hook and passes those exact addresses to the connector, preventing
97
+ * hostname-to-private-IP and DNS-rebinding bypasses. Other runtimes should
98
+ * constrain egress at the network layer when handling untrusted URLs.
99
+ *
95
100
  * @throws DownloadError if a hop is unsafe, the redirect limit is exceeded, or
96
101
  * a redirect cannot be validated on a non-browser runtime.
97
102
  */
@@ -945,9 +950,8 @@ declare function convertToBase64(value: string | Uint8Array): string;
945
950
  * Validates that a URL is safe to download from, blocking private/internal addresses
946
951
  * to prevent SSRF attacks.
947
952
  *
948
- * Note: this performs string/literal-IP checks only. It does not resolve DNS, so a
949
- * hostname that resolves to a private address is not blocked here (see callers, which
950
- * should additionally constrain egress at the network layer when handling untrusted URLs).
953
+ * Note: this function performs string/literal-IP checks only. The Node.js
954
+ * download fetch additionally validates and pins DNS results at connect time.
951
955
  *
952
956
  * @param url - The URL string to validate.
953
957
  * @throws DownloadError if the URL is unsafe.
package/dist/index.js CHANGED
@@ -322,6 +322,19 @@ function validateDownloadUrl(url) {
322
322
  return;
323
323
  }
324
324
  }
325
+ function validateDownloadAddress({
326
+ address,
327
+ family,
328
+ hostname
329
+ }) {
330
+ const isUnsafe = family === 4 ? !isIPv4(address) || isPrivateIPv4(address) : family === 6 ? isPrivateIPv6(address) : true;
331
+ if (isUnsafe) {
332
+ throw new DownloadError({
333
+ url: hostname,
334
+ message: `Hostname ${hostname} resolved to disallowed IP address ${address}`
335
+ });
336
+ }
337
+ }
325
338
  function isIPv4(hostname) {
326
339
  const parts = hostname.split(".");
327
340
  if (parts.length !== 4) return false;
@@ -408,6 +421,104 @@ function isPrivateIPv6(ip) {
408
421
  return false;
409
422
  }
410
423
 
424
+ // src/safe-node-fetch.ts
425
+ function createSafeLookup(lookup) {
426
+ return ((hostname, options, callback) => {
427
+ lookup(hostname, { ...options, all: true }, (error, addresses) => {
428
+ if (error) {
429
+ callback(error);
430
+ return;
431
+ }
432
+ try {
433
+ const [firstAddress] = addresses;
434
+ if (firstAddress == null) {
435
+ throw new Error(`Hostname ${hostname} did not resolve to an address`);
436
+ }
437
+ for (const { address, family } of addresses) {
438
+ validateDownloadAddress({ address, family, hostname });
439
+ }
440
+ if (options.all === true) {
441
+ callback(null, addresses);
442
+ } else {
443
+ callback(
444
+ null,
445
+ firstAddress.address,
446
+ firstAddress.family
447
+ );
448
+ }
449
+ } catch (error2) {
450
+ callback(
451
+ error2 instanceof Error ? error2 : new Error(String(error2))
452
+ );
453
+ }
454
+ });
455
+ });
456
+ }
457
+ var safeNodeFetchPromise;
458
+ var initialGlobalFetch = globalThis.fetch;
459
+ var initialGlobalFetchIsNodeDefault = isNodeDefaultFetch(initialGlobalFetch);
460
+ function isNodeRuntime() {
461
+ var _a2, _b2;
462
+ const runtimeProcess = globalThis.process;
463
+ return ((_a2 = runtimeProcess == null ? void 0 : runtimeProcess.release) == null ? void 0 : _a2.name) === "node" && ((_b2 = runtimeProcess.versions) == null ? void 0 : _b2.bun) == null;
464
+ }
465
+ async function getDefaultDownloadFetch() {
466
+ if (!isNodeRuntime() || !initialGlobalFetchIsNodeDefault || globalThis.fetch !== initialGlobalFetch) {
467
+ return globalThis.fetch;
468
+ }
469
+ return safeNodeFetchPromise != null ? safeNodeFetchPromise : safeNodeFetchPromise = createSafeNodeFetch();
470
+ }
471
+ function isNodeDefaultFetch(fetch) {
472
+ const source = Function.prototype.toString.call(fetch);
473
+ return source.includes("internal/deps/undici") || source.includes("lazy loading of undici");
474
+ }
475
+ async function createSafeNodeFetch() {
476
+ const [{ createRequire }, { lookup }] = await Promise.all([
477
+ loadNodeModule("node:module"),
478
+ loadNodeModule("node:dns")
479
+ ]);
480
+ const { Agent, fetch } = createRequire(getCurrentModulePath())(
481
+ "undici"
482
+ );
483
+ const dispatcher = new Agent({
484
+ connect: {
485
+ lookup: createSafeLookup(lookup)
486
+ }
487
+ });
488
+ return ((input, init) => fetch(
489
+ input,
490
+ {
491
+ ...init,
492
+ dispatcher
493
+ }
494
+ ));
495
+ }
496
+ async function loadNodeModule(id) {
497
+ var _a2;
498
+ const processWithBuiltins = globalThis.process;
499
+ const builtinModule = (_a2 = processWithBuiltins == null ? void 0 : processWithBuiltins.getBuiltinModule) == null ? void 0 : _a2.call(processWithBuiltins, id);
500
+ return builtinModule == null ? await importNodeModule(id) : builtinModule;
501
+ }
502
+ function importNodeModule(id) {
503
+ return import(id);
504
+ }
505
+ function getCurrentModulePath() {
506
+ const originalPrepareStackTrace = Error.prepareStackTrace;
507
+ try {
508
+ Error.prepareStackTrace = (_error, callSites) => callSites;
509
+ const error = new Error("Capture current module path");
510
+ Error.captureStackTrace(error, getCurrentModulePath);
511
+ const [caller] = error.stack;
512
+ const fileName = caller == null ? void 0 : caller.getFileName();
513
+ if (fileName == null) {
514
+ throw new Error("Unable to determine the current module path");
515
+ }
516
+ return fileName;
517
+ } finally {
518
+ Error.prepareStackTrace = originalPrepareStackTrace;
519
+ }
520
+ }
521
+
411
522
  // src/fetch-with-validated-redirects.ts
412
523
  var MAX_DOWNLOAD_REDIRECTS = 10;
413
524
  async function fetchWithValidatedRedirects({
@@ -423,6 +534,7 @@ async function fetchWithValidatedRedirects({
423
534
  let currentUrl = url;
424
535
  for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount++) {
425
536
  validateDownloadUrl(currentUrl);
537
+ const fetch = await getDefaultDownloadFetch();
426
538
  const response = await fetch(currentUrl, {
427
539
  ...baseInit,
428
540
  redirect: "manual"
@@ -640,7 +752,7 @@ function withUserAgentSuffix(headers, ...userAgentSuffixParts) {
640
752
  }
641
753
 
642
754
  // src/version.ts
643
- var VERSION = true ? "3.0.30" : "0.0.0-test";
755
+ var VERSION = true ? "3.0.31" : "0.0.0-test";
644
756
 
645
757
  // src/get-from-api.ts
646
758
  var getOriginalFetch = () => globalThis.fetch;
@@ -650,10 +762,10 @@ var getFromApi = async ({
650
762
  successfulResponseHandler,
651
763
  failedResponseHandler,
652
764
  abortSignal,
653
- fetch: fetch2 = getOriginalFetch()
765
+ fetch = getOriginalFetch()
654
766
  }) => {
655
767
  try {
656
- const response = await fetch2(url, {
768
+ const response = await fetch(url, {
657
769
  method: "GET",
658
770
  headers: withUserAgentSuffix(
659
771
  headers,
@@ -1099,7 +1211,7 @@ var postJsonToApi = async ({
1099
1211
  failedResponseHandler,
1100
1212
  successfulResponseHandler,
1101
1213
  abortSignal,
1102
- fetch: fetch2
1214
+ fetch
1103
1215
  }) => postToApi({
1104
1216
  url,
1105
1217
  headers: {
@@ -1113,7 +1225,7 @@ var postJsonToApi = async ({
1113
1225
  failedResponseHandler,
1114
1226
  successfulResponseHandler,
1115
1227
  abortSignal,
1116
- fetch: fetch2
1228
+ fetch
1117
1229
  });
1118
1230
  var postFormDataToApi = async ({
1119
1231
  url,
@@ -1122,7 +1234,7 @@ var postFormDataToApi = async ({
1122
1234
  failedResponseHandler,
1123
1235
  successfulResponseHandler,
1124
1236
  abortSignal,
1125
- fetch: fetch2
1237
+ fetch
1126
1238
  }) => postToApi({
1127
1239
  url,
1128
1240
  headers,
@@ -1133,7 +1245,7 @@ var postFormDataToApi = async ({
1133
1245
  failedResponseHandler,
1134
1246
  successfulResponseHandler,
1135
1247
  abortSignal,
1136
- fetch: fetch2
1248
+ fetch
1137
1249
  });
1138
1250
  var postToApi = async ({
1139
1251
  url,
@@ -1142,10 +1254,10 @@ var postToApi = async ({
1142
1254
  successfulResponseHandler,
1143
1255
  failedResponseHandler,
1144
1256
  abortSignal,
1145
- fetch: fetch2 = getOriginalFetch2()
1257
+ fetch = getOriginalFetch2()
1146
1258
  }) => {
1147
1259
  try {
1148
- const response = await fetch2(url, {
1260
+ const response = await fetch(url, {
1149
1261
  method: "POST",
1150
1262
  headers: withUserAgentSuffix(
1151
1263
  headers,