@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 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
- * Not solved here: this does string/literal checks only and does not resolve
860
- * DNS, so a hostname that *resolves* to a private address, and DNS rebinding
861
- * (the resolved IP flipping between validation and connect), are not blocked.
862
- * Server deployments fetching untrusted URLs should constrain egress at the
863
- * network layer or inject a Node `fetch` that pins the resolved IP at connect
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. It does not resolve DNS, so a
2427
- * hostname that resolves to a private address is not blocked here (see callers, which
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 = globalThis.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
- if (trustedOrigin === void 0 || !isSameOrigin(currentUrl, trustedOrigin)) {
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.14" : "0.0.0-test";
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 = getOriginalFetch(),
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 fetch(url, {
1326
+ }) : await requestFetch(url, {
1215
1327
  method: "GET",
1216
1328
  headers: requestHeaders,
1217
1329
  signal: abortSignal