@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 +6 -0
- package/dist/index.d.mts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +121 -9
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +121 -9
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/fetch-with-validated-redirects.ts +7 -0
- package/src/safe-node-fetch.ts +209 -0
- package/src/validate-download-url.ts +30 -3
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.
|
|
1415
|
-
*
|
|
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.
|
|
1415
|
-
*
|
|
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.
|
|
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
|
|
904
|
+
fetch = getOriginalFetch()
|
|
793
905
|
}) => {
|
|
794
906
|
try {
|
|
795
|
-
const response = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2702
|
+
fetch = getOriginalFetch2()
|
|
2591
2703
|
}) => {
|
|
2592
2704
|
try {
|
|
2593
|
-
const response = await
|
|
2705
|
+
const response = await fetch(url, {
|
|
2594
2706
|
method: "POST",
|
|
2595
2707
|
headers: withUserAgentSuffix(
|
|
2596
2708
|
headers,
|