@ai-sdk/provider-utils 4.0.17 → 4.0.19
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 +12 -0
- package/dist/index.d.mts +10 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +103 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +102 -4
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/download-blob.ts +2 -0
- package/src/index.ts +1 -0
- package/src/secure-json-parse.ts +6 -2
- package/src/validate-download-url.ts +143 -0
package/package.json
CHANGED
package/src/download-blob.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
readResponseWithSizeLimit,
|
|
4
4
|
DEFAULT_MAX_DOWNLOAD_SIZE,
|
|
5
5
|
} from './read-response-with-size-limit';
|
|
6
|
+
import { validateDownloadUrl } from './validate-download-url';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Download a file from a URL and return it as a Blob.
|
|
@@ -19,6 +20,7 @@ export async function downloadBlob(
|
|
|
19
20
|
url: string,
|
|
20
21
|
options?: { maxBytes?: number; abortSignal?: AbortSignal },
|
|
21
22
|
): Promise<Blob> {
|
|
23
|
+
validateDownloadUrl(url);
|
|
22
24
|
try {
|
|
23
25
|
const response = await fetch(url, {
|
|
24
26
|
signal: options?.abortSignal,
|
package/src/index.ts
CHANGED
|
@@ -56,6 +56,7 @@ export {
|
|
|
56
56
|
} from './schema';
|
|
57
57
|
export { stripFileExtension } from './strip-file-extension';
|
|
58
58
|
export * from './uint8-utils';
|
|
59
|
+
export { validateDownloadUrl } from './validate-download-url';
|
|
59
60
|
export * from './validate-types';
|
|
60
61
|
export { VERSION } from './version';
|
|
61
62
|
export { withUserAgentSuffix } from './with-user-agent-suffix';
|
package/src/secure-json-parse.ts
CHANGED
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
//
|
|
22
22
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
23
23
|
|
|
24
|
-
const suspectProtoRx =
|
|
25
|
-
|
|
24
|
+
const suspectProtoRx =
|
|
25
|
+
/"(?:_|\\u005[Ff])(?:_|\\u005[Ff])(?:p|\\u0070)(?:r|\\u0072)(?:o|\\u006[Ff])(?:t|\\u0074)(?:o|\\u006[Ff])(?:_|\\u005[Ff])(?:_|\\u005[Ff])"\s*:/;
|
|
26
|
+
const suspectConstructorRx =
|
|
27
|
+
/"(?:c|\\u0063)(?:o|\\u006[Ff])(?:n|\\u006[Ee])(?:s|\\u0073)(?:t|\\u0074)(?:r|\\u0072)(?:u|\\u0075)(?:c|\\u0063)(?:t|\\u0074)(?:o|\\u006[Ff])(?:r|\\u0072)"\s*:/;
|
|
26
28
|
|
|
27
29
|
function _parse(text: string) {
|
|
28
30
|
// Parse normally
|
|
@@ -58,6 +60,8 @@ function filter(obj: any) {
|
|
|
58
60
|
|
|
59
61
|
if (
|
|
60
62
|
Object.prototype.hasOwnProperty.call(node, 'constructor') &&
|
|
63
|
+
node.constructor !== null &&
|
|
64
|
+
typeof node.constructor === 'object' &&
|
|
61
65
|
Object.prototype.hasOwnProperty.call(node.constructor, 'prototype')
|
|
62
66
|
) {
|
|
63
67
|
throw new SyntaxError('Object contains forbidden prototype property');
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { DownloadError } from './download-error';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validates that a URL is safe to download from, blocking private/internal addresses
|
|
5
|
+
* to prevent SSRF attacks.
|
|
6
|
+
*
|
|
7
|
+
* @param url - The URL string to validate.
|
|
8
|
+
* @throws DownloadError if the URL is unsafe.
|
|
9
|
+
*/
|
|
10
|
+
export function validateDownloadUrl(url: string): void {
|
|
11
|
+
let parsed: URL;
|
|
12
|
+
try {
|
|
13
|
+
parsed = new URL(url);
|
|
14
|
+
} catch {
|
|
15
|
+
throw new DownloadError({
|
|
16
|
+
url,
|
|
17
|
+
message: `Invalid URL: ${url}`,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Only allow http and https protocols
|
|
22
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
23
|
+
throw new DownloadError({
|
|
24
|
+
url,
|
|
25
|
+
message: `URL scheme must be http or https, got ${parsed.protocol}`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const hostname = parsed.hostname;
|
|
30
|
+
|
|
31
|
+
// Block empty hostname
|
|
32
|
+
if (!hostname) {
|
|
33
|
+
throw new DownloadError({
|
|
34
|
+
url,
|
|
35
|
+
message: `URL must have a hostname`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Block localhost and .local domains
|
|
40
|
+
if (
|
|
41
|
+
hostname === 'localhost' ||
|
|
42
|
+
hostname.endsWith('.local') ||
|
|
43
|
+
hostname.endsWith('.localhost')
|
|
44
|
+
) {
|
|
45
|
+
throw new DownloadError({
|
|
46
|
+
url,
|
|
47
|
+
message: `URL with hostname ${hostname} is not allowed`,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check for IPv6 addresses (enclosed in brackets in URLs)
|
|
52
|
+
if (hostname.startsWith('[') && hostname.endsWith(']')) {
|
|
53
|
+
const ipv6 = hostname.slice(1, -1);
|
|
54
|
+
if (isPrivateIPv6(ipv6)) {
|
|
55
|
+
throw new DownloadError({
|
|
56
|
+
url,
|
|
57
|
+
message: `URL with IPv6 address ${hostname} is not allowed`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Check for IPv4 addresses
|
|
64
|
+
if (isIPv4(hostname)) {
|
|
65
|
+
if (isPrivateIPv4(hostname)) {
|
|
66
|
+
throw new DownloadError({
|
|
67
|
+
url,
|
|
68
|
+
message: `URL with IP address ${hostname} is not allowed`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isIPv4(hostname: string): boolean {
|
|
76
|
+
const parts = hostname.split('.');
|
|
77
|
+
if (parts.length !== 4) return false;
|
|
78
|
+
return parts.every(part => {
|
|
79
|
+
const num = Number(part);
|
|
80
|
+
return (
|
|
81
|
+
Number.isInteger(num) && num >= 0 && num <= 255 && String(num) === part
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function isPrivateIPv4(ip: string): boolean {
|
|
87
|
+
const parts = ip.split('.').map(Number);
|
|
88
|
+
const [a, b] = parts;
|
|
89
|
+
|
|
90
|
+
// 0.0.0.0/8
|
|
91
|
+
if (a === 0) return true;
|
|
92
|
+
// 10.0.0.0/8
|
|
93
|
+
if (a === 10) return true;
|
|
94
|
+
// 127.0.0.0/8
|
|
95
|
+
if (a === 127) return true;
|
|
96
|
+
// 169.254.0.0/16
|
|
97
|
+
if (a === 169 && b === 254) return true;
|
|
98
|
+
// 172.16.0.0/12
|
|
99
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
100
|
+
// 192.168.0.0/16
|
|
101
|
+
if (a === 192 && b === 168) return true;
|
|
102
|
+
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isPrivateIPv6(ip: string): boolean {
|
|
107
|
+
const normalized = ip.toLowerCase();
|
|
108
|
+
|
|
109
|
+
// ::1 (loopback)
|
|
110
|
+
if (normalized === '::1') return true;
|
|
111
|
+
// :: (unspecified)
|
|
112
|
+
if (normalized === '::') return true;
|
|
113
|
+
|
|
114
|
+
// Check for IPv4-mapped addresses (::ffff:x.x.x.x or ::ffff:HHHH:HHHH)
|
|
115
|
+
if (normalized.startsWith('::ffff:')) {
|
|
116
|
+
const mappedPart = normalized.slice(7);
|
|
117
|
+
// Dotted-decimal form: ::ffff:127.0.0.1
|
|
118
|
+
if (isIPv4(mappedPart)) {
|
|
119
|
+
return isPrivateIPv4(mappedPart);
|
|
120
|
+
}
|
|
121
|
+
// Hex form: ::ffff:7f00:1 (URL parser normalizes to this)
|
|
122
|
+
const hexParts = mappedPart.split(':');
|
|
123
|
+
if (hexParts.length === 2) {
|
|
124
|
+
const high = parseInt(hexParts[0], 16);
|
|
125
|
+
const low = parseInt(hexParts[1], 16);
|
|
126
|
+
if (!isNaN(high) && !isNaN(low)) {
|
|
127
|
+
const a = (high >> 8) & 0xff;
|
|
128
|
+
const b = high & 0xff;
|
|
129
|
+
const c = (low >> 8) & 0xff;
|
|
130
|
+
const d = low & 0xff;
|
|
131
|
+
return isPrivateIPv4(`${a}.${b}.${c}.${d}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// fc00::/7 (unique local addresses - fc00:: and fd00::)
|
|
137
|
+
if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true;
|
|
138
|
+
|
|
139
|
+
// fe80::/10 (link-local)
|
|
140
|
+
if (normalized.startsWith('fe80')) return true;
|
|
141
|
+
|
|
142
|
+
return false;
|
|
143
|
+
}
|