@ai-sdk/provider-utils 4.0.40 → 4.0.41

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
+ ## 4.0.41
4
+
5
+ ### Patch Changes
6
+
7
+ - 9ecdefe: 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
  ## 4.0.40
4
10
 
5
11
  ### Patch Changes
package/dist/index.d.mts CHANGED
@@ -223,6 +223,11 @@ declare function readResponseWithSizeLimit({ response, url, maxBytes, }: {
223
223
  * The returned response is the final (non-redirect) response. The caller is
224
224
  * responsible for checking `response.ok` and reading the body.
225
225
  *
226
+ * On Node.js, the default fetch resolves every hostname through a validating
227
+ * lookup hook and passes those exact addresses to the connector, preventing
228
+ * hostname-to-private-IP and DNS-rebinding bypasses. Other runtimes should
229
+ * constrain egress at the network layer when handling untrusted URLs.
230
+ *
226
231
  * @throws DownloadError if a hop is unsafe, the redirect limit is exceeded, or
227
232
  * a redirect cannot be validated on a non-browser runtime.
228
233
  */
@@ -1411,9 +1416,8 @@ declare function convertToBase64(value: string | Uint8Array): string;
1411
1416
  * Validates that a URL is safe to download from, blocking private/internal addresses
1412
1417
  * to prevent SSRF attacks.
1413
1418
  *
1414
- * Note: this performs string/literal-IP checks only. It does not resolve DNS, so a
1415
- * hostname that resolves to a private address is not blocked here (see callers, which
1416
- * should additionally constrain egress at the network layer when handling untrusted URLs).
1419
+ * Note: this function performs string/literal-IP checks only. The Node.js
1420
+ * download fetch additionally validates and pins DNS results at connect time.
1417
1421
  *
1418
1422
  * @param url - The URL string to validate.
1419
1423
  * @throws DownloadError if the URL is unsafe.
package/dist/index.d.ts CHANGED
@@ -223,6 +223,11 @@ declare function readResponseWithSizeLimit({ response, url, maxBytes, }: {
223
223
  * The returned response is the final (non-redirect) response. The caller is
224
224
  * responsible for checking `response.ok` and reading the body.
225
225
  *
226
+ * On Node.js, the default fetch resolves every hostname through a validating
227
+ * lookup hook and passes those exact addresses to the connector, preventing
228
+ * hostname-to-private-IP and DNS-rebinding bypasses. Other runtimes should
229
+ * constrain egress at the network layer when handling untrusted URLs.
230
+ *
226
231
  * @throws DownloadError if a hop is unsafe, the redirect limit is exceeded, or
227
232
  * a redirect cannot be validated on a non-browser runtime.
228
233
  */
@@ -1411,9 +1416,8 @@ declare function convertToBase64(value: string | Uint8Array): string;
1411
1416
  * Validates that a URL is safe to download from, blocking private/internal addresses
1412
1417
  * to prevent SSRF attacks.
1413
1418
  *
1414
- * Note: this performs string/literal-IP checks only. It does not resolve DNS, so a
1415
- * hostname that resolves to a private address is not blocked here (see callers, which
1416
- * should additionally constrain egress at the network layer when handling untrusted URLs).
1419
+ * Note: this function performs string/literal-IP checks only. The Node.js
1420
+ * download fetch additionally validates and pins DNS results at connect time.
1417
1421
  *
1418
1422
  * @param url - The URL string to validate.
1419
1423
  * @throws DownloadError if the URL is unsafe.
package/dist/index.js CHANGED
@@ -402,6 +402,19 @@ function validateDownloadUrl(url) {
402
402
  return;
403
403
  }
404
404
  }
405
+ function validateDownloadAddress({
406
+ address,
407
+ family,
408
+ hostname
409
+ }) {
410
+ const isUnsafe = family === 4 ? !isIPv4(address) || isPrivateIPv4(address) : family === 6 ? isPrivateIPv6(address) : true;
411
+ if (isUnsafe) {
412
+ throw new DownloadError({
413
+ url: hostname,
414
+ message: `Hostname ${hostname} resolved to disallowed IP address ${address}`
415
+ });
416
+ }
417
+ }
405
418
  function isIPv4(hostname) {
406
419
  const parts = hostname.split(".");
407
420
  if (parts.length !== 4) return false;
@@ -488,6 +501,104 @@ function isPrivateIPv6(ip) {
488
501
  return false;
489
502
  }
490
503
 
504
+ // src/safe-node-fetch.ts
505
+ function createSafeLookup(lookup) {
506
+ return ((hostname, options, callback) => {
507
+ lookup(hostname, { ...options, all: true }, (error, addresses) => {
508
+ if (error) {
509
+ callback(error);
510
+ return;
511
+ }
512
+ try {
513
+ const [firstAddress] = addresses;
514
+ if (firstAddress == null) {
515
+ throw new Error(`Hostname ${hostname} did not resolve to an address`);
516
+ }
517
+ for (const { address, family } of addresses) {
518
+ validateDownloadAddress({ address, family, hostname });
519
+ }
520
+ if (options.all === true) {
521
+ callback(null, addresses);
522
+ } else {
523
+ callback(
524
+ null,
525
+ firstAddress.address,
526
+ firstAddress.family
527
+ );
528
+ }
529
+ } catch (error2) {
530
+ callback(
531
+ error2 instanceof Error ? error2 : new Error(String(error2))
532
+ );
533
+ }
534
+ });
535
+ });
536
+ }
537
+ var safeNodeFetchPromise;
538
+ var initialGlobalFetch = globalThis.fetch;
539
+ var initialGlobalFetchIsNodeDefault = isNodeDefaultFetch(initialGlobalFetch);
540
+ function isNodeRuntime() {
541
+ var _a2, _b2;
542
+ const runtimeProcess = globalThis.process;
543
+ return ((_a2 = runtimeProcess == null ? void 0 : runtimeProcess.release) == null ? void 0 : _a2.name) === "node" && ((_b2 = runtimeProcess.versions) == null ? void 0 : _b2.bun) == null;
544
+ }
545
+ async function getDefaultDownloadFetch() {
546
+ if (!isNodeRuntime() || !initialGlobalFetchIsNodeDefault || globalThis.fetch !== initialGlobalFetch) {
547
+ return globalThis.fetch;
548
+ }
549
+ return safeNodeFetchPromise != null ? safeNodeFetchPromise : safeNodeFetchPromise = createSafeNodeFetch();
550
+ }
551
+ function isNodeDefaultFetch(fetch) {
552
+ const source = Function.prototype.toString.call(fetch);
553
+ return source.includes("internal/deps/undici") || source.includes("lazy loading of undici");
554
+ }
555
+ async function createSafeNodeFetch() {
556
+ const [{ createRequire }, { lookup }] = await Promise.all([
557
+ loadNodeModule("node:module"),
558
+ loadNodeModule("node:dns")
559
+ ]);
560
+ const { Agent, fetch } = createRequire(getCurrentModulePath())(
561
+ "undici"
562
+ );
563
+ const dispatcher = new Agent({
564
+ connect: {
565
+ lookup: createSafeLookup(lookup)
566
+ }
567
+ });
568
+ return ((input, init) => fetch(
569
+ input,
570
+ {
571
+ ...init,
572
+ dispatcher
573
+ }
574
+ ));
575
+ }
576
+ async function loadNodeModule(id) {
577
+ var _a2;
578
+ const processWithBuiltins = globalThis.process;
579
+ const builtinModule = (_a2 = processWithBuiltins == null ? void 0 : processWithBuiltins.getBuiltinModule) == null ? void 0 : _a2.call(processWithBuiltins, id);
580
+ return builtinModule == null ? await importNodeModule(id) : builtinModule;
581
+ }
582
+ function importNodeModule(id) {
583
+ return import(id);
584
+ }
585
+ function getCurrentModulePath() {
586
+ const originalPrepareStackTrace = Error.prepareStackTrace;
587
+ try {
588
+ Error.prepareStackTrace = (_error, callSites) => callSites;
589
+ const error = new Error("Capture current module path");
590
+ Error.captureStackTrace(error, getCurrentModulePath);
591
+ const [caller] = error.stack;
592
+ const fileName = caller == null ? void 0 : caller.getFileName();
593
+ if (fileName == null) {
594
+ throw new Error("Unable to determine the current module path");
595
+ }
596
+ return fileName;
597
+ } finally {
598
+ Error.prepareStackTrace = originalPrepareStackTrace;
599
+ }
600
+ }
601
+
491
602
  // src/fetch-with-validated-redirects.ts
492
603
  var MAX_DOWNLOAD_REDIRECTS = 10;
493
604
  async function fetchWithValidatedRedirects({
@@ -503,6 +614,7 @@ async function fetchWithValidatedRedirects({
503
614
  let currentUrl = url;
504
615
  for (let redirectCount = 0; redirectCount <= maxRedirects; redirectCount++) {
505
616
  validateDownloadUrl(currentUrl);
617
+ const fetch = await getDefaultDownloadFetch();
506
618
  const response = await fetch(currentUrl, {
507
619
  ...baseInit,
508
620
  redirect: "manual"
@@ -779,7 +891,7 @@ function withUserAgentSuffix(headers, ...userAgentSuffixParts) {
779
891
  }
780
892
 
781
893
  // src/version.ts
782
- var VERSION = true ? "4.0.40" : "0.0.0-test";
894
+ var VERSION = true ? "4.0.41" : "0.0.0-test";
783
895
 
784
896
  // src/get-from-api.ts
785
897
  var getOriginalFetch = () => globalThis.fetch;
@@ -789,10 +901,10 @@ var getFromApi = async ({
789
901
  successfulResponseHandler,
790
902
  failedResponseHandler,
791
903
  abortSignal,
792
- fetch: fetch2 = getOriginalFetch()
904
+ fetch = getOriginalFetch()
793
905
  }) => {
794
906
  try {
795
- const response = await fetch2(url, {
907
+ const response = await fetch(url, {
796
908
  method: "GET",
797
909
  headers: withUserAgentSuffix(
798
910
  headers,
@@ -2544,7 +2656,7 @@ var postJsonToApi = async ({
2544
2656
  failedResponseHandler,
2545
2657
  successfulResponseHandler,
2546
2658
  abortSignal,
2547
- fetch: fetch2
2659
+ fetch
2548
2660
  }) => postToApi({
2549
2661
  url,
2550
2662
  headers: {
@@ -2558,7 +2670,7 @@ var postJsonToApi = async ({
2558
2670
  failedResponseHandler,
2559
2671
  successfulResponseHandler,
2560
2672
  abortSignal,
2561
- fetch: fetch2
2673
+ fetch
2562
2674
  });
2563
2675
  var postFormDataToApi = async ({
2564
2676
  url,
@@ -2567,7 +2679,7 @@ var postFormDataToApi = async ({
2567
2679
  failedResponseHandler,
2568
2680
  successfulResponseHandler,
2569
2681
  abortSignal,
2570
- fetch: fetch2
2682
+ fetch
2571
2683
  }) => postToApi({
2572
2684
  url,
2573
2685
  headers,
@@ -2578,7 +2690,7 @@ var postFormDataToApi = async ({
2578
2690
  failedResponseHandler,
2579
2691
  successfulResponseHandler,
2580
2692
  abortSignal,
2581
- fetch: fetch2
2693
+ fetch
2582
2694
  });
2583
2695
  var postToApi = async ({
2584
2696
  url,
@@ -2587,10 +2699,10 @@ var postToApi = async ({
2587
2699
  successfulResponseHandler,
2588
2700
  failedResponseHandler,
2589
2701
  abortSignal,
2590
- fetch: fetch2 = getOriginalFetch2()
2702
+ fetch = getOriginalFetch2()
2591
2703
  }) => {
2592
2704
  try {
2593
- const response = await fetch2(url, {
2705
+ const response = await fetch(url, {
2594
2706
  method: "POST",
2595
2707
  headers: withUserAgentSuffix(
2596
2708
  headers,