@jami-studio/core 0.92.25 → 0.92.26
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/corpus/core/CHANGELOG.md +6 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/extensions/url-safety.ts +363 -306
- package/corpus/templates/content/actions/list-comments.ts +1 -2
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/extensions/url-safety.d.ts +14 -0
- package/dist/extensions/url-safety.d.ts.map +1 -1
- package/dist/extensions/url-safety.js +62 -2
- package/dist/extensions/url-safety.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/package.json +1 -1
package/corpus/core/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @agent-native/core
|
|
2
2
|
|
|
3
|
+
## 0.92.26
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 64f78fe: ssrfSafeFetch trusts the deployment's OWN configured origins (APP_URL, BETTER_AUTH_URL, WEBHOOK_BASE_URL, WORKSPACE_GATEWAY_URL, workspace app manifest URLs). These are operator configuration, not user input, so a fetch to them is a self-call — the private-address guard was blocking every workspace-internal A2A call (call-agent to a sibling app) on local dev and self-hosted private networks. Only exact origin matches are trusted; redirect hops are still re-validated, so a trusted origin cannot 30x into the private network, and the default posture with no configured origins is unchanged.
|
|
8
|
+
|
|
3
9
|
## 0.92.25
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/corpus/core/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.92.
|
|
3
|
+
"version": "0.92.26",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/studio-jami/jami-studio#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -1,306 +1,363 @@
|
|
|
1
|
-
const METADATA_HOSTS = [
|
|
2
|
-
"metadata.google.internal",
|
|
3
|
-
"metadata.google.internal.",
|
|
4
|
-
];
|
|
5
|
-
|
|
6
|
-
const DNS_REBIND_SUFFIXES = [
|
|
7
|
-
".nip.io",
|
|
8
|
-
".sslip.io",
|
|
9
|
-
".xip.io",
|
|
10
|
-
".localtest.me",
|
|
11
|
-
".lvh.me",
|
|
12
|
-
];
|
|
13
|
-
|
|
14
|
-
function isPrivateIpv4(a: number, b: number, c = 0, d = 0): boolean {
|
|
15
|
-
if (![a, b, c, d].every((part) => part >= 0 && part <= 255)) return true;
|
|
16
|
-
if (a === 127) return true;
|
|
17
|
-
if (a === 10) return true;
|
|
18
|
-
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
19
|
-
if (a === 192 && b === 168) return true;
|
|
20
|
-
if (a === 169 && b === 254) return true;
|
|
21
|
-
if (a === 0) return true;
|
|
22
|
-
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
23
|
-
if (a === 192 && b === 0) return true;
|
|
24
|
-
if (a === 198 && (b === 18 || b === 19)) return true;
|
|
25
|
-
if (a === 192 && b === 0 && c === 2) return true;
|
|
26
|
-
if (a === 198 && b === 51 && c === 100) return true;
|
|
27
|
-
if (a === 203 && b === 0 && c === 113) return true;
|
|
28
|
-
if (a >= 224) return true;
|
|
29
|
-
return false;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function isPrivateIpv4MappedHex(host: string): boolean {
|
|
33
|
-
const mapped = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
|
|
34
|
-
if (!mapped) return false;
|
|
35
|
-
const high = Number.parseInt(mapped[1], 16);
|
|
36
|
-
const low = Number.parseInt(mapped[2], 16);
|
|
37
|
-
if (high < 0 || high > 0xffff || low < 0 || low > 0xffff) return false;
|
|
38
|
-
const a = (high >> 8) & 0xff;
|
|
39
|
-
const b = high & 0xff;
|
|
40
|
-
const c = (low >> 8) & 0xff;
|
|
41
|
-
const d = low & 0xff;
|
|
42
|
-
return isPrivateIpv4(a, b, c, d);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function isPrivateHost(hostname: string): boolean {
|
|
46
|
-
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
47
|
-
if (
|
|
48
|
-
host === "localhost" ||
|
|
49
|
-
host === "::1" ||
|
|
50
|
-
host === "::0" ||
|
|
51
|
-
host === "::"
|
|
52
|
-
) {
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
if (METADATA_HOSTS.includes(host)) return true;
|
|
56
|
-
|
|
57
|
-
// IPv6 ULA/link-local/multicast.
|
|
58
|
-
if (/^f[cd]/.test(host) || /^fe[89ab]/.test(host)) return true;
|
|
59
|
-
if (/^ff/i.test(host)) return true;
|
|
60
|
-
|
|
61
|
-
// IPv4-mapped IPv6. URL parsing may preserve dotted form in some runtimes
|
|
62
|
-
// or normalize it to hex, e.g. [::ffff:127.0.0.1] -> ::ffff:7f00:1.
|
|
63
|
-
const v4mappedDotted = host.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
64
|
-
if (v4mappedDotted) {
|
|
65
|
-
const [a, b, c, d] = v4mappedDotted[1].split(".").map(Number);
|
|
66
|
-
if (isPrivateIpv4(a, b, c, d)) return true;
|
|
67
|
-
}
|
|
68
|
-
if (isPrivateIpv4MappedHex(host)) return true;
|
|
69
|
-
|
|
70
|
-
// Dotted IPv4. URL parsing normalizes shorthand/octal/hex IPv4 forms to
|
|
71
|
-
// dotted decimal before we reach this point.
|
|
72
|
-
const parts = host.split(".");
|
|
73
|
-
if (parts.length === 4 && parts.every((p) => /^\d+$/.test(p))) {
|
|
74
|
-
const [a, b, c, d] = parts.map(Number);
|
|
75
|
-
if (isPrivateIpv4(a, b, c, d)) return true;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Decimal integer IPv4.
|
|
79
|
-
if (/^\d+$/.test(host)) {
|
|
80
|
-
const num = Number(host);
|
|
81
|
-
if (num >= 0 && num <= 0xffffffff) {
|
|
82
|
-
const a = (num >>> 24) & 0xff;
|
|
83
|
-
const b = (num >>> 16) & 0xff;
|
|
84
|
-
const c = (num >>> 8) & 0xff;
|
|
85
|
-
const d = num & 0xff;
|
|
86
|
-
if (isPrivateIpv4(a, b, c, d)) return true;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
return false;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function isBlockedExtensionUrl(url: string): boolean {
|
|
94
|
-
try {
|
|
95
|
-
const parsed = new URL(url);
|
|
96
|
-
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
97
|
-
return true;
|
|
98
|
-
}
|
|
99
|
-
const host = parsed.hostname.toLowerCase();
|
|
100
|
-
if (isPrivateHost(host)) return true;
|
|
101
|
-
if (
|
|
102
|
-
DNS_REBIND_SUFFIXES.some((suffix) => {
|
|
103
|
-
const bare = suffix.slice(1);
|
|
104
|
-
return host === bare || host.endsWith(suffix);
|
|
105
|
-
})
|
|
106
|
-
) {
|
|
107
|
-
return true;
|
|
108
|
-
}
|
|
109
|
-
} catch {
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function isIpLiteralHost(hostname: string): boolean {
|
|
116
|
-
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
117
|
-
if (host.includes(":")) return true;
|
|
118
|
-
const parts = host.split(".");
|
|
119
|
-
return parts.length === 4 && parts.every((p) => /^\d+$/.test(p));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Async SSRF guard for environments that can resolve DNS. The synchronous
|
|
124
|
-
* guard catches literals and known rebinding domains; this closes the common
|
|
125
|
-
* "public hostname resolves to a private address" gap before dispatch.
|
|
126
|
-
*/
|
|
127
|
-
export async function isBlockedExtensionUrlWithDns(
|
|
128
|
-
url: string,
|
|
129
|
-
): Promise<boolean> {
|
|
130
|
-
if (isBlockedExtensionUrl(url)) return true;
|
|
131
|
-
|
|
132
|
-
let hostname: string;
|
|
133
|
-
try {
|
|
134
|
-
hostname = new URL(url).hostname.toLowerCase();
|
|
135
|
-
} catch {
|
|
136
|
-
return true;
|
|
137
|
-
}
|
|
138
|
-
if (!hostname || isIpLiteralHost(hostname)) return false;
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
const { lookup } = await import("node:dns/promises");
|
|
142
|
-
const records = await lookup(hostname, { all: true, verbatim: true });
|
|
143
|
-
return records.some((record) => isPrivateHost(record.address));
|
|
144
|
-
} catch {
|
|
145
|
-
// Some edge runtimes do not expose DNS lookup. Keep the deterministic
|
|
146
|
-
// parser-based protections instead of failing every outbound request.
|
|
147
|
-
return false;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
export
|
|
305
|
-
|
|
306
|
-
|
|
1
|
+
const METADATA_HOSTS = [
|
|
2
|
+
"metadata.google.internal",
|
|
3
|
+
"metadata.google.internal.",
|
|
4
|
+
];
|
|
5
|
+
|
|
6
|
+
const DNS_REBIND_SUFFIXES = [
|
|
7
|
+
".nip.io",
|
|
8
|
+
".sslip.io",
|
|
9
|
+
".xip.io",
|
|
10
|
+
".localtest.me",
|
|
11
|
+
".lvh.me",
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
function isPrivateIpv4(a: number, b: number, c = 0, d = 0): boolean {
|
|
15
|
+
if (![a, b, c, d].every((part) => part >= 0 && part <= 255)) return true;
|
|
16
|
+
if (a === 127) return true;
|
|
17
|
+
if (a === 10) return true;
|
|
18
|
+
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
19
|
+
if (a === 192 && b === 168) return true;
|
|
20
|
+
if (a === 169 && b === 254) return true;
|
|
21
|
+
if (a === 0) return true;
|
|
22
|
+
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
23
|
+
if (a === 192 && b === 0) return true;
|
|
24
|
+
if (a === 198 && (b === 18 || b === 19)) return true;
|
|
25
|
+
if (a === 192 && b === 0 && c === 2) return true;
|
|
26
|
+
if (a === 198 && b === 51 && c === 100) return true;
|
|
27
|
+
if (a === 203 && b === 0 && c === 113) return true;
|
|
28
|
+
if (a >= 224) return true;
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isPrivateIpv4MappedHex(host: string): boolean {
|
|
33
|
+
const mapped = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
|
|
34
|
+
if (!mapped) return false;
|
|
35
|
+
const high = Number.parseInt(mapped[1], 16);
|
|
36
|
+
const low = Number.parseInt(mapped[2], 16);
|
|
37
|
+
if (high < 0 || high > 0xffff || low < 0 || low > 0xffff) return false;
|
|
38
|
+
const a = (high >> 8) & 0xff;
|
|
39
|
+
const b = high & 0xff;
|
|
40
|
+
const c = (low >> 8) & 0xff;
|
|
41
|
+
const d = low & 0xff;
|
|
42
|
+
return isPrivateIpv4(a, b, c, d);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isPrivateHost(hostname: string): boolean {
|
|
46
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
47
|
+
if (
|
|
48
|
+
host === "localhost" ||
|
|
49
|
+
host === "::1" ||
|
|
50
|
+
host === "::0" ||
|
|
51
|
+
host === "::"
|
|
52
|
+
) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (METADATA_HOSTS.includes(host)) return true;
|
|
56
|
+
|
|
57
|
+
// IPv6 ULA/link-local/multicast.
|
|
58
|
+
if (/^f[cd]/.test(host) || /^fe[89ab]/.test(host)) return true;
|
|
59
|
+
if (/^ff/i.test(host)) return true;
|
|
60
|
+
|
|
61
|
+
// IPv4-mapped IPv6. URL parsing may preserve dotted form in some runtimes
|
|
62
|
+
// or normalize it to hex, e.g. [::ffff:127.0.0.1] -> ::ffff:7f00:1.
|
|
63
|
+
const v4mappedDotted = host.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
64
|
+
if (v4mappedDotted) {
|
|
65
|
+
const [a, b, c, d] = v4mappedDotted[1].split(".").map(Number);
|
|
66
|
+
if (isPrivateIpv4(a, b, c, d)) return true;
|
|
67
|
+
}
|
|
68
|
+
if (isPrivateIpv4MappedHex(host)) return true;
|
|
69
|
+
|
|
70
|
+
// Dotted IPv4. URL parsing normalizes shorthand/octal/hex IPv4 forms to
|
|
71
|
+
// dotted decimal before we reach this point.
|
|
72
|
+
const parts = host.split(".");
|
|
73
|
+
if (parts.length === 4 && parts.every((p) => /^\d+$/.test(p))) {
|
|
74
|
+
const [a, b, c, d] = parts.map(Number);
|
|
75
|
+
if (isPrivateIpv4(a, b, c, d)) return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Decimal integer IPv4.
|
|
79
|
+
if (/^\d+$/.test(host)) {
|
|
80
|
+
const num = Number(host);
|
|
81
|
+
if (num >= 0 && num <= 0xffffffff) {
|
|
82
|
+
const a = (num >>> 24) & 0xff;
|
|
83
|
+
const b = (num >>> 16) & 0xff;
|
|
84
|
+
const c = (num >>> 8) & 0xff;
|
|
85
|
+
const d = num & 0xff;
|
|
86
|
+
if (isPrivateIpv4(a, b, c, d)) return true;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function isBlockedExtensionUrl(url: string): boolean {
|
|
94
|
+
try {
|
|
95
|
+
const parsed = new URL(url);
|
|
96
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
const host = parsed.hostname.toLowerCase();
|
|
100
|
+
if (isPrivateHost(host)) return true;
|
|
101
|
+
if (
|
|
102
|
+
DNS_REBIND_SUFFIXES.some((suffix) => {
|
|
103
|
+
const bare = suffix.slice(1);
|
|
104
|
+
return host === bare || host.endsWith(suffix);
|
|
105
|
+
})
|
|
106
|
+
) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isIpLiteralHost(hostname: string): boolean {
|
|
116
|
+
const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
117
|
+
if (host.includes(":")) return true;
|
|
118
|
+
const parts = host.split(".");
|
|
119
|
+
return parts.length === 4 && parts.every((p) => /^\d+$/.test(p));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Async SSRF guard for environments that can resolve DNS. The synchronous
|
|
124
|
+
* guard catches literals and known rebinding domains; this closes the common
|
|
125
|
+
* "public hostname resolves to a private address" gap before dispatch.
|
|
126
|
+
*/
|
|
127
|
+
export async function isBlockedExtensionUrlWithDns(
|
|
128
|
+
url: string,
|
|
129
|
+
): Promise<boolean> {
|
|
130
|
+
if (isBlockedExtensionUrl(url)) return true;
|
|
131
|
+
|
|
132
|
+
let hostname: string;
|
|
133
|
+
try {
|
|
134
|
+
hostname = new URL(url).hostname.toLowerCase();
|
|
135
|
+
} catch {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
if (!hostname || isIpLiteralHost(hostname)) return false;
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const { lookup } = await import("node:dns/promises");
|
|
142
|
+
const records = await lookup(hostname, { all: true, verbatim: true });
|
|
143
|
+
return records.some((record) => isPrivateHost(record.address));
|
|
144
|
+
} catch {
|
|
145
|
+
// Some edge runtimes do not expose DNS lookup. Keep the deterministic
|
|
146
|
+
// parser-based protections instead of failing every outbound request.
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Origins the deployment itself declares as its own surfaces. These come from
|
|
153
|
+
* operator-controlled configuration (env), NOT from user/agent input, so a
|
|
154
|
+
* fetch to them is a self-call, not an SSRF: cross-app A2A on a workspace
|
|
155
|
+
* (call-agent) targets sibling apps at the deployment's own base URL, and in
|
|
156
|
+
* local dev / self-hosted private networks that origin is a loopback or
|
|
157
|
+
* RFC-1918 address the private-address guard would otherwise block — which
|
|
158
|
+
* broke every workspace-internal agent call on `wrangler pages dev` and the
|
|
159
|
+
* dev gateway. Only EXACT origin matches (scheme + host + port) are trusted;
|
|
160
|
+
* everything else keeps the full SSRF policy (including redirect hops).
|
|
161
|
+
*/
|
|
162
|
+
export function trustedInternalOrigins(): Set<string> {
|
|
163
|
+
const origins = new Set<string>();
|
|
164
|
+
const addUrl = (value: unknown) => {
|
|
165
|
+
if (typeof value !== "string" || !value.trim()) return;
|
|
166
|
+
try {
|
|
167
|
+
origins.add(new URL(value).origin);
|
|
168
|
+
} catch {
|
|
169
|
+
// Ignore malformed configured URLs — they never matched anything.
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const env = (globalThis as any)?.process?.env ?? {};
|
|
173
|
+
addUrl(env.APP_URL);
|
|
174
|
+
addUrl(env.BETTER_AUTH_URL);
|
|
175
|
+
addUrl(env.WEBHOOK_BASE_URL);
|
|
176
|
+
addUrl(env.WORKSPACE_GATEWAY_URL);
|
|
177
|
+
const manifestJson = env.AGENT_NATIVE_WORKSPACE_APPS_JSON;
|
|
178
|
+
if (typeof manifestJson === "string" && manifestJson.trim()) {
|
|
179
|
+
try {
|
|
180
|
+
const apps = JSON.parse(manifestJson);
|
|
181
|
+
if (Array.isArray(apps)) {
|
|
182
|
+
for (const app of apps) addUrl(app?.url);
|
|
183
|
+
}
|
|
184
|
+
} catch {
|
|
185
|
+
// Malformed manifest — no trusted origins from it.
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return origins;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** True when `url`'s origin exactly matches a configured internal origin. */
|
|
192
|
+
export function isTrustedInternalUrl(url: string): boolean {
|
|
193
|
+
try {
|
|
194
|
+
return trustedInternalOrigins().has(new URL(url).origin);
|
|
195
|
+
} catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Build an undici Dispatcher whose connect-time DNS lookup runs through a
|
|
202
|
+
* private-IP guard. This closes the TOCTOU gap where:
|
|
203
|
+
* 1. We resolve hostname → public IP and pass.
|
|
204
|
+
* 2. Between that lookup and the actual connect, DNS rebinding flips the
|
|
205
|
+
* record to a private IP.
|
|
206
|
+
* 3. fetch() resolves again and connects to the private IP.
|
|
207
|
+
*
|
|
208
|
+
* With a custom dispatcher, the same lookup that produces the IP also gates
|
|
209
|
+
* the connect: if the IP is in the private set, the connect throws.
|
|
210
|
+
*
|
|
211
|
+
* Returns `null` if undici / node:dns are not available (e.g. some edge
|
|
212
|
+
* runtimes); the caller should fall back to the regular `fetch` path —
|
|
213
|
+
* `isBlockedExtensionUrlWithDns` will still have caught most rebinding cases.
|
|
214
|
+
*/
|
|
215
|
+
export async function createSsrfSafeDispatcher(): Promise<unknown | null> {
|
|
216
|
+
// Keep the optional undici import opaque to Vite/Rolldown. A static
|
|
217
|
+
// `import("undici")` makes browser builds try to resolve and bundle undici
|
|
218
|
+
// even though this dispatcher is only useful in Node server runtimes.
|
|
219
|
+
let undici: any;
|
|
220
|
+
let dnsModule: any;
|
|
221
|
+
try {
|
|
222
|
+
const runtimeImport = new Function(
|
|
223
|
+
"specifier",
|
|
224
|
+
"return import(specifier)",
|
|
225
|
+
) as (specifier: string) => Promise<any>;
|
|
226
|
+
undici = await runtimeImport("undici");
|
|
227
|
+
dnsModule = await import("node:dns");
|
|
228
|
+
} catch {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const { Agent } = undici;
|
|
233
|
+
const { lookup } = dnsModule;
|
|
234
|
+
if (!Agent || !lookup) return null;
|
|
235
|
+
|
|
236
|
+
return new Agent({
|
|
237
|
+
connect: {
|
|
238
|
+
// Override DNS lookup at connect time so the IP we hand to undici's
|
|
239
|
+
// socket is the one we authorized. Reject any record in the private
|
|
240
|
+
// set BEFORE the TCP handshake.
|
|
241
|
+
lookup: (
|
|
242
|
+
hostname: string,
|
|
243
|
+
options: any,
|
|
244
|
+
callback: (
|
|
245
|
+
err: NodeJS.ErrnoException | null,
|
|
246
|
+
address?: string | { address: string; family: number }[],
|
|
247
|
+
family?: number,
|
|
248
|
+
) => void,
|
|
249
|
+
) => {
|
|
250
|
+
lookup(
|
|
251
|
+
hostname,
|
|
252
|
+
{ all: true, verbatim: true },
|
|
253
|
+
(err: NodeJS.ErrnoException | null, addresses: any) => {
|
|
254
|
+
if (err) return callback(err);
|
|
255
|
+
const list: { address: string; family: number }[] = Array.isArray(
|
|
256
|
+
addresses,
|
|
257
|
+
)
|
|
258
|
+
? addresses
|
|
259
|
+
: [{ address: addresses, family: 4 }];
|
|
260
|
+
for (const record of list) {
|
|
261
|
+
if (isPrivateHost(record.address)) {
|
|
262
|
+
const e = new Error(
|
|
263
|
+
`Connect blocked: ${hostname} resolved to private address ${record.address}`,
|
|
264
|
+
) as NodeJS.ErrnoException;
|
|
265
|
+
e.code = "EAI_BLOCKED";
|
|
266
|
+
return callback(e);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// Mirror Node's lookup behavior: when `all` is true, return the
|
|
270
|
+
// array; otherwise the first entry. undici's connect honors
|
|
271
|
+
// `options.all`.
|
|
272
|
+
if (options && options.all) {
|
|
273
|
+
return callback(null, list as any);
|
|
274
|
+
}
|
|
275
|
+
const first = list[0];
|
|
276
|
+
return callback(null, first.address, first.family);
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* SSRF-safe `fetch` for any server-side request to a user/agent-supplied URL.
|
|
286
|
+
*
|
|
287
|
+
* Applies the same protections the extension proxy uses, so every call site
|
|
288
|
+
* that fetches an untrusted URL gets them without re-implementing the loop:
|
|
289
|
+
* 1. Pre-flight DNS-aware private-address check (isBlockedExtensionUrlWithDns)
|
|
290
|
+
* on the initial URL and on every redirect hop.
|
|
291
|
+
* 2. A connect-time dispatcher that re-checks the resolved IP at TCP-connect
|
|
292
|
+
* time (closes the DNS-rebinding TOCTOU) when undici is available.
|
|
293
|
+
* 3. Manual redirect handling — a public URL cannot 30x-redirect into the
|
|
294
|
+
* private network because each hop is re-validated before it is followed.
|
|
295
|
+
*
|
|
296
|
+
* Throws an Error whose message starts with "SSRF blocked:" when a target
|
|
297
|
+
* (initial or via redirect) resolves to a private/internal address, or when the
|
|
298
|
+
* redirect limit is exceeded. Otherwise returns the final Response.
|
|
299
|
+
*
|
|
300
|
+
* `httpsOnly` extends the per-hop validation to the URL scheme: redirects are
|
|
301
|
+
* followed only to `https:` targets, so an HTTPS-only caller cannot be
|
|
302
|
+
* downgraded to plain HTTP by a 30x from the (untrusted) origin.
|
|
303
|
+
*/
|
|
304
|
+
export async function ssrfSafeFetch(
|
|
305
|
+
url: string,
|
|
306
|
+
init: RequestInit = {},
|
|
307
|
+
options: { maxRedirects?: number; httpsOnly?: boolean } = {},
|
|
308
|
+
): Promise<Response> {
|
|
309
|
+
const maxRedirects = options.maxRedirects ?? 3;
|
|
310
|
+
const dispatcher = (await createSsrfSafeDispatcher()) ?? undefined;
|
|
311
|
+
|
|
312
|
+
let currentUrl = url;
|
|
313
|
+
for (let hop = 0; hop <= maxRedirects; hop++) {
|
|
314
|
+
if (options.httpsOnly && new URL(currentUrl).protocol !== "https:") {
|
|
315
|
+
throw new Error(
|
|
316
|
+
`SSRF blocked: refusing to fetch non-HTTPS address (${currentUrl})`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
// The deployment's own configured origins are self-calls (workspace A2A,
|
|
320
|
+
// self-dispatch), not attacker-controlled targets — skip only the
|
|
321
|
+
// private-address block for exact origin matches. Each redirect hop is
|
|
322
|
+
// still re-validated, so a trusted origin cannot 30x into the private
|
|
323
|
+
// network.
|
|
324
|
+
const trustedHop = isTrustedInternalUrl(currentUrl);
|
|
325
|
+
if (!trustedHop && (await isBlockedExtensionUrlWithDns(currentUrl))) {
|
|
326
|
+
throw new Error(
|
|
327
|
+
`SSRF blocked: refusing to fetch private/internal address (${currentUrl})`,
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
const fetchOpts: RequestInit & { dispatcher?: unknown } = {
|
|
331
|
+
...init,
|
|
332
|
+
redirect: "manual",
|
|
333
|
+
};
|
|
334
|
+
// The connect-time guard would also reject a trusted internal origin's
|
|
335
|
+
// loopback/private IP at the TCP layer — attach it only to untrusted hops.
|
|
336
|
+
if (dispatcher && !trustedHop) fetchOpts.dispatcher = dispatcher;
|
|
337
|
+
|
|
338
|
+
const response = await fetch(currentUrl, fetchOpts);
|
|
339
|
+
if (response.status >= 300 && response.status < 400) {
|
|
340
|
+
const location = response.headers.get("location");
|
|
341
|
+
if (!location) return response;
|
|
342
|
+
// Drain the redirect body so the hop's connection is released instead
|
|
343
|
+
// of being held until GC.
|
|
344
|
+
await response.body?.cancel().catch(() => {});
|
|
345
|
+
currentUrl = new URL(location, currentUrl).href;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
return response;
|
|
349
|
+
}
|
|
350
|
+
throw new Error(
|
|
351
|
+
`SSRF blocked: too many redirects (>${maxRedirects}) while fetching ${url}`,
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
356
|
+
// Legacy aliases — predate the Tools → Extensions rename. Templates import
|
|
357
|
+
// these via the legacy `@agent-native/core/tools/url-safety` subpath; keep
|
|
358
|
+
// the names exported so they keep resolving until every consumer updates.
|
|
359
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
export { isBlockedExtensionUrl as isBlockedToolUrl };
|
|
362
|
+
export { isBlockedExtensionUrlWithDns as isBlockedToolUrlWithDns };
|
|
363
|
+
export { ssrfSafeFetch as ssrfSafeToolFetch };
|
|
@@ -32,12 +32,11 @@ function parseMentions(value: string | null): Mention[] {
|
|
|
32
32
|
export default defineAction({
|
|
33
33
|
description: "List all comments on a document, grouped by thread.",
|
|
34
34
|
schema: z.object({
|
|
35
|
-
documentId: z.string().
|
|
35
|
+
documentId: z.string().min(1).describe("Document ID (required)"),
|
|
36
36
|
}),
|
|
37
37
|
http: { method: "GET" },
|
|
38
38
|
run: async (args) => {
|
|
39
39
|
const documentId = args.documentId;
|
|
40
|
-
if (!documentId) throw new Error("--documentId is required");
|
|
41
40
|
|
|
42
41
|
const access = await assertAccess("document", documentId, "viewer");
|
|
43
42
|
const ownerEmail = access.resource.ownerEmail as string;
|
|
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
|
|
|
62
62
|
error: string;
|
|
63
63
|
states?: undefined;
|
|
64
64
|
} | {
|
|
65
|
-
error?: undefined;
|
|
66
65
|
states: {
|
|
67
66
|
clientId: number;
|
|
68
67
|
state: string;
|
|
69
68
|
}[];
|
|
69
|
+
error?: undefined;
|
|
70
70
|
}>>;
|
|
71
71
|
/**
|
|
72
72
|
* GET /_agent-native/collab/:docId/users
|
|
@@ -77,10 +77,10 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
|
|
|
77
77
|
error: string;
|
|
78
78
|
users?: undefined;
|
|
79
79
|
} | {
|
|
80
|
-
error?: undefined;
|
|
81
80
|
users: {
|
|
82
81
|
clientId: number;
|
|
83
82
|
lastSeen: number;
|
|
84
83
|
}[];
|
|
84
|
+
error?: undefined;
|
|
85
85
|
}>>;
|
|
86
86
|
//# sourceMappingURL=awareness.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"awareness.d.ts","sourceRoot":"","sources":["../../src/collab/awareness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAgB3C,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAOD,eAAO,MAAM,sBAAsB,EAAG,kBAA2B,CAAC;AAElE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,MAAM,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAOD,wBAAgB,mBAAmB,IAAI,YAAY,CAElD;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,cAAc,GAAG,SAAS,GAChC,IAAI,CAON;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,KAAK,CAAC,EAAE,cAAc,GACrB,IAAI,CAgBN;AAoBD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,MAAmB,GAC7B,IAAI,CAON;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE1E;AAiBD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAO1E;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,IAAI,CAOnE;AAkCD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa
|
|
1
|
+
{"version":3,"file":"awareness.d.ts","sourceRoot":"","sources":["../../src/collab/awareness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAgB3C,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAOD,eAAO,MAAM,sBAAsB,EAAG,kBAA2B,CAAC;AAElE,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,WAAW,CAAC;IACpB,IAAI,EAAE,kBAAkB,CAAC;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,MAAM,EAAE,KAAK,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnD,gFAAgF;IAChF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAOD,wBAAgB,mBAAmB,IAAI,YAAY,CAElD;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,cAAc,GAAG,SAAS,GAChC,IAAI,CAON;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,KAAK,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,EAClD,KAAK,CAAC,EAAE,cAAc,GACrB,IAAI,CAgBN;AAoBD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,SAAS,GAAE,MAAmB,GAC7B,IAAI,CAON;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAE1E;AAiBD,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAO1E;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,GAAG,IAAI,CAOnE;AAkCD;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;;kBAwDa,MAAM;eAAS,MAAM;;;GAoB1D,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,cAAc;;;;;kBAYM,MAAM;kBAAY,MAAM;;;GAMvD,CAAC"}
|
|
@@ -5,6 +5,20 @@ export declare function isBlockedExtensionUrl(url: string): boolean;
|
|
|
5
5
|
* "public hostname resolves to a private address" gap before dispatch.
|
|
6
6
|
*/
|
|
7
7
|
export declare function isBlockedExtensionUrlWithDns(url: string): Promise<boolean>;
|
|
8
|
+
/**
|
|
9
|
+
* Origins the deployment itself declares as its own surfaces. These come from
|
|
10
|
+
* operator-controlled configuration (env), NOT from user/agent input, so a
|
|
11
|
+
* fetch to them is a self-call, not an SSRF: cross-app A2A on a workspace
|
|
12
|
+
* (call-agent) targets sibling apps at the deployment's own base URL, and in
|
|
13
|
+
* local dev / self-hosted private networks that origin is a loopback or
|
|
14
|
+
* RFC-1918 address the private-address guard would otherwise block — which
|
|
15
|
+
* broke every workspace-internal agent call on `wrangler pages dev` and the
|
|
16
|
+
* dev gateway. Only EXACT origin matches (scheme + host + port) are trusted;
|
|
17
|
+
* everything else keeps the full SSRF policy (including redirect hops).
|
|
18
|
+
*/
|
|
19
|
+
export declare function trustedInternalOrigins(): Set<string>;
|
|
20
|
+
/** True when `url`'s origin exactly matches a configured internal origin. */
|
|
21
|
+
export declare function isTrustedInternalUrl(url: string): boolean;
|
|
8
22
|
/**
|
|
9
23
|
* Build an undici Dispatcher whose connect-time DNS lookup runs through a
|
|
10
24
|
* private-IP guard. This closes the TOCTOU gap where:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"url-safety.d.ts","sourceRoot":"","sources":["../../src/extensions/url-safety.ts"],"names":[],"mappings":"AA4FA,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAoB1D;AASD;;;;GAIG;AACH,wBAAsB,4BAA4B,CAChD,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,OAAO,CAAC,CAoBlB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAmExE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,WAAgB,EACtB,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAO,GAC3D,OAAO,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"url-safety.d.ts","sourceRoot":"","sources":["../../src/extensions/url-safety.ts"],"names":[],"mappings":"AA4FA,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAoB1D;AASD;;;;GAIG;AACH,wBAAsB,4BAA4B,CAChD,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,OAAO,CAAC,CAoBlB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,IAAI,GAAG,CAAC,MAAM,CAAC,CA2BpD;AAED,6EAA6E;AAC7E,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAMzD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAmExE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,MAAM,EACX,IAAI,GAAE,WAAgB,EACtB,OAAO,GAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAO,GAC3D,OAAO,CAAC,QAAQ,CAAC,CA6CnB;AAQD,OAAO,EAAE,qBAAqB,IAAI,gBAAgB,EAAE,CAAC;AACrD,OAAO,EAAE,4BAA4B,IAAI,uBAAuB,EAAE,CAAC;AACnE,OAAO,EAAE,aAAa,IAAI,iBAAiB,EAAE,CAAC"}
|
|
@@ -157,6 +157,58 @@ export async function isBlockedExtensionUrlWithDns(url) {
|
|
|
157
157
|
return false;
|
|
158
158
|
}
|
|
159
159
|
}
|
|
160
|
+
/**
|
|
161
|
+
* Origins the deployment itself declares as its own surfaces. These come from
|
|
162
|
+
* operator-controlled configuration (env), NOT from user/agent input, so a
|
|
163
|
+
* fetch to them is a self-call, not an SSRF: cross-app A2A on a workspace
|
|
164
|
+
* (call-agent) targets sibling apps at the deployment's own base URL, and in
|
|
165
|
+
* local dev / self-hosted private networks that origin is a loopback or
|
|
166
|
+
* RFC-1918 address the private-address guard would otherwise block — which
|
|
167
|
+
* broke every workspace-internal agent call on `wrangler pages dev` and the
|
|
168
|
+
* dev gateway. Only EXACT origin matches (scheme + host + port) are trusted;
|
|
169
|
+
* everything else keeps the full SSRF policy (including redirect hops).
|
|
170
|
+
*/
|
|
171
|
+
export function trustedInternalOrigins() {
|
|
172
|
+
const origins = new Set();
|
|
173
|
+
const addUrl = (value) => {
|
|
174
|
+
if (typeof value !== "string" || !value.trim())
|
|
175
|
+
return;
|
|
176
|
+
try {
|
|
177
|
+
origins.add(new URL(value).origin);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Ignore malformed configured URLs — they never matched anything.
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
const env = globalThis?.process?.env ?? {};
|
|
184
|
+
addUrl(env.APP_URL);
|
|
185
|
+
addUrl(env.BETTER_AUTH_URL);
|
|
186
|
+
addUrl(env.WEBHOOK_BASE_URL);
|
|
187
|
+
addUrl(env.WORKSPACE_GATEWAY_URL);
|
|
188
|
+
const manifestJson = env.AGENT_NATIVE_WORKSPACE_APPS_JSON;
|
|
189
|
+
if (typeof manifestJson === "string" && manifestJson.trim()) {
|
|
190
|
+
try {
|
|
191
|
+
const apps = JSON.parse(manifestJson);
|
|
192
|
+
if (Array.isArray(apps)) {
|
|
193
|
+
for (const app of apps)
|
|
194
|
+
addUrl(app?.url);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Malformed manifest — no trusted origins from it.
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return origins;
|
|
202
|
+
}
|
|
203
|
+
/** True when `url`'s origin exactly matches a configured internal origin. */
|
|
204
|
+
export function isTrustedInternalUrl(url) {
|
|
205
|
+
try {
|
|
206
|
+
return trustedInternalOrigins().has(new URL(url).origin);
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
160
212
|
/**
|
|
161
213
|
* Build an undici Dispatcher whose connect-time DNS lookup runs through a
|
|
162
214
|
* private-IP guard. This closes the TOCTOU gap where:
|
|
@@ -250,14 +302,22 @@ export async function ssrfSafeFetch(url, init = {}, options = {}) {
|
|
|
250
302
|
if (options.httpsOnly && new URL(currentUrl).protocol !== "https:") {
|
|
251
303
|
throw new Error(`SSRF blocked: refusing to fetch non-HTTPS address (${currentUrl})`);
|
|
252
304
|
}
|
|
253
|
-
|
|
305
|
+
// The deployment's own configured origins are self-calls (workspace A2A,
|
|
306
|
+
// self-dispatch), not attacker-controlled targets — skip only the
|
|
307
|
+
// private-address block for exact origin matches. Each redirect hop is
|
|
308
|
+
// still re-validated, so a trusted origin cannot 30x into the private
|
|
309
|
+
// network.
|
|
310
|
+
const trustedHop = isTrustedInternalUrl(currentUrl);
|
|
311
|
+
if (!trustedHop && (await isBlockedExtensionUrlWithDns(currentUrl))) {
|
|
254
312
|
throw new Error(`SSRF blocked: refusing to fetch private/internal address (${currentUrl})`);
|
|
255
313
|
}
|
|
256
314
|
const fetchOpts = {
|
|
257
315
|
...init,
|
|
258
316
|
redirect: "manual",
|
|
259
317
|
};
|
|
260
|
-
|
|
318
|
+
// The connect-time guard would also reject a trusted internal origin's
|
|
319
|
+
// loopback/private IP at the TCP layer — attach it only to untrusted hops.
|
|
320
|
+
if (dispatcher && !trustedHop)
|
|
261
321
|
fetchOpts.dispatcher = dispatcher;
|
|
262
322
|
const response = await fetch(currentUrl, fetchOpts);
|
|
263
323
|
if (response.status >= 300 && response.status < 400) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"url-safety.js","sourceRoot":"","sources":["../../src/extensions/url-safety.ts"],"names":[],"mappings":"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","sourcesContent":["const METADATA_HOSTS = [\r\n \"metadata.google.internal\",\r\n \"metadata.google.internal.\",\r\n];\r\n\r\nconst DNS_REBIND_SUFFIXES = [\r\n \".nip.io\",\r\n \".sslip.io\",\r\n \".xip.io\",\r\n \".localtest.me\",\r\n \".lvh.me\",\r\n];\r\n\r\nfunction isPrivateIpv4(a: number, b: number, c = 0, d = 0): boolean {\r\n if (![a, b, c, d].every((part) => part >= 0 && part <= 255)) return true;\r\n if (a === 127) return true;\r\n if (a === 10) return true;\r\n if (a === 172 && b >= 16 && b <= 31) return true;\r\n if (a === 192 && b === 168) return true;\r\n if (a === 169 && b === 254) return true;\r\n if (a === 0) return true;\r\n if (a === 100 && b >= 64 && b <= 127) return true;\r\n if (a === 192 && b === 0) return true;\r\n if (a === 198 && (b === 18 || b === 19)) return true;\r\n if (a === 192 && b === 0 && c === 2) return true;\r\n if (a === 198 && b === 51 && c === 100) return true;\r\n if (a === 203 && b === 0 && c === 113) return true;\r\n if (a >= 224) return true;\r\n return false;\r\n}\r\n\r\nfunction isPrivateIpv4MappedHex(host: string): boolean {\r\n const mapped = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);\r\n if (!mapped) return false;\r\n const high = Number.parseInt(mapped[1], 16);\r\n const low = Number.parseInt(mapped[2], 16);\r\n if (high < 0 || high > 0xffff || low < 0 || low > 0xffff) return false;\r\n const a = (high >> 8) & 0xff;\r\n const b = high & 0xff;\r\n const c = (low >> 8) & 0xff;\r\n const d = low & 0xff;\r\n return isPrivateIpv4(a, b, c, d);\r\n}\r\n\r\nfunction isPrivateHost(hostname: string): boolean {\r\n const host = hostname.toLowerCase().replace(/^\\[|\\]$/g, \"\");\r\n if (\r\n host === \"localhost\" ||\r\n host === \"::1\" ||\r\n host === \"::0\" ||\r\n host === \"::\"\r\n ) {\r\n return true;\r\n }\r\n if (METADATA_HOSTS.includes(host)) return true;\r\n\r\n // IPv6 ULA/link-local/multicast.\r\n if (/^f[cd]/.test(host) || /^fe[89ab]/.test(host)) return true;\r\n if (/^ff/i.test(host)) return true;\r\n\r\n // IPv4-mapped IPv6. URL parsing may preserve dotted form in some runtimes\r\n // or normalize it to hex, e.g. [::ffff:127.0.0.1] -> ::ffff:7f00:1.\r\n const v4mappedDotted = host.match(/^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/);\r\n if (v4mappedDotted) {\r\n const [a, b, c, d] = v4mappedDotted[1].split(\".\").map(Number);\r\n if (isPrivateIpv4(a, b, c, d)) return true;\r\n }\r\n if (isPrivateIpv4MappedHex(host)) return true;\r\n\r\n // Dotted IPv4. URL parsing normalizes shorthand/octal/hex IPv4 forms to\r\n // dotted decimal before we reach this point.\r\n const parts = host.split(\".\");\r\n if (parts.length === 4 && parts.every((p) => /^\\d+$/.test(p))) {\r\n const [a, b, c, d] = parts.map(Number);\r\n if (isPrivateIpv4(a, b, c, d)) return true;\r\n }\r\n\r\n // Decimal integer IPv4.\r\n if (/^\\d+$/.test(host)) {\r\n const num = Number(host);\r\n if (num >= 0 && num <= 0xffffffff) {\r\n const a = (num >>> 24) & 0xff;\r\n const b = (num >>> 16) & 0xff;\r\n const c = (num >>> 8) & 0xff;\r\n const d = num & 0xff;\r\n if (isPrivateIpv4(a, b, c, d)) return true;\r\n }\r\n }\r\n\r\n return false;\r\n}\r\n\r\nexport function isBlockedExtensionUrl(url: string): boolean {\r\n try {\r\n const parsed = new URL(url);\r\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\r\n return true;\r\n }\r\n const host = parsed.hostname.toLowerCase();\r\n if (isPrivateHost(host)) return true;\r\n if (\r\n DNS_REBIND_SUFFIXES.some((suffix) => {\r\n const bare = suffix.slice(1);\r\n return host === bare || host.endsWith(suffix);\r\n })\r\n ) {\r\n return true;\r\n }\r\n } catch {\r\n return true;\r\n }\r\n return false;\r\n}\r\n\r\nfunction isIpLiteralHost(hostname: string): boolean {\r\n const host = hostname.toLowerCase().replace(/^\\[|\\]$/g, \"\");\r\n if (host.includes(\":\")) return true;\r\n const parts = host.split(\".\");\r\n return parts.length === 4 && parts.every((p) => /^\\d+$/.test(p));\r\n}\r\n\r\n/**\r\n * Async SSRF guard for environments that can resolve DNS. The synchronous\r\n * guard catches literals and known rebinding domains; this closes the common\r\n * \"public hostname resolves to a private address\" gap before dispatch.\r\n */\r\nexport async function isBlockedExtensionUrlWithDns(\r\n url: string,\r\n): Promise<boolean> {\r\n if (isBlockedExtensionUrl(url)) return true;\r\n\r\n let hostname: string;\r\n try {\r\n hostname = new URL(url).hostname.toLowerCase();\r\n } catch {\r\n return true;\r\n }\r\n if (!hostname || isIpLiteralHost(hostname)) return false;\r\n\r\n try {\r\n const { lookup } = await import(\"node:dns/promises\");\r\n const records = await lookup(hostname, { all: true, verbatim: true });\r\n return records.some((record) => isPrivateHost(record.address));\r\n } catch {\r\n // Some edge runtimes do not expose DNS lookup. Keep the deterministic\r\n // parser-based protections instead of failing every outbound request.\r\n return false;\r\n }\r\n}\r\n\r\n/**\r\n * Build an undici Dispatcher whose connect-time DNS lookup runs through a\r\n * private-IP guard. This closes the TOCTOU gap where:\r\n * 1. We resolve hostname → public IP and pass.\r\n * 2. Between that lookup and the actual connect, DNS rebinding flips the\r\n * record to a private IP.\r\n * 3. fetch() resolves again and connects to the private IP.\r\n *\r\n * With a custom dispatcher, the same lookup that produces the IP also gates\r\n * the connect: if the IP is in the private set, the connect throws.\r\n *\r\n * Returns `null` if undici / node:dns are not available (e.g. some edge\r\n * runtimes); the caller should fall back to the regular `fetch` path —\r\n * `isBlockedExtensionUrlWithDns` will still have caught most rebinding cases.\r\n */\r\nexport async function createSsrfSafeDispatcher(): Promise<unknown | null> {\r\n // Keep the optional undici import opaque to Vite/Rolldown. A static\r\n // `import(\"undici\")` makes browser builds try to resolve and bundle undici\r\n // even though this dispatcher is only useful in Node server runtimes.\r\n let undici: any;\r\n let dnsModule: any;\r\n try {\r\n const runtimeImport = new Function(\r\n \"specifier\",\r\n \"return import(specifier)\",\r\n ) as (specifier: string) => Promise<any>;\r\n undici = await runtimeImport(\"undici\");\r\n dnsModule = await import(\"node:dns\");\r\n } catch {\r\n return null;\r\n }\r\n\r\n const { Agent } = undici;\r\n const { lookup } = dnsModule;\r\n if (!Agent || !lookup) return null;\r\n\r\n return new Agent({\r\n connect: {\r\n // Override DNS lookup at connect time so the IP we hand to undici's\r\n // socket is the one we authorized. Reject any record in the private\r\n // set BEFORE the TCP handshake.\r\n lookup: (\r\n hostname: string,\r\n options: any,\r\n callback: (\r\n err: NodeJS.ErrnoException | null,\r\n address?: string | { address: string; family: number }[],\r\n family?: number,\r\n ) => void,\r\n ) => {\r\n lookup(\r\n hostname,\r\n { all: true, verbatim: true },\r\n (err: NodeJS.ErrnoException | null, addresses: any) => {\r\n if (err) return callback(err);\r\n const list: { address: string; family: number }[] = Array.isArray(\r\n addresses,\r\n )\r\n ? addresses\r\n : [{ address: addresses, family: 4 }];\r\n for (const record of list) {\r\n if (isPrivateHost(record.address)) {\r\n const e = new Error(\r\n `Connect blocked: ${hostname} resolved to private address ${record.address}`,\r\n ) as NodeJS.ErrnoException;\r\n e.code = \"EAI_BLOCKED\";\r\n return callback(e);\r\n }\r\n }\r\n // Mirror Node's lookup behavior: when `all` is true, return the\r\n // array; otherwise the first entry. undici's connect honors\r\n // `options.all`.\r\n if (options && options.all) {\r\n return callback(null, list as any);\r\n }\r\n const first = list[0];\r\n return callback(null, first.address, first.family);\r\n },\r\n );\r\n },\r\n },\r\n });\r\n}\r\n\r\n/**\r\n * SSRF-safe `fetch` for any server-side request to a user/agent-supplied URL.\r\n *\r\n * Applies the same protections the extension proxy uses, so every call site\r\n * that fetches an untrusted URL gets them without re-implementing the loop:\r\n * 1. Pre-flight DNS-aware private-address check (isBlockedExtensionUrlWithDns)\r\n * on the initial URL and on every redirect hop.\r\n * 2. A connect-time dispatcher that re-checks the resolved IP at TCP-connect\r\n * time (closes the DNS-rebinding TOCTOU) when undici is available.\r\n * 3. Manual redirect handling — a public URL cannot 30x-redirect into the\r\n * private network because each hop is re-validated before it is followed.\r\n *\r\n * Throws an Error whose message starts with \"SSRF blocked:\" when a target\r\n * (initial or via redirect) resolves to a private/internal address, or when the\r\n * redirect limit is exceeded. Otherwise returns the final Response.\r\n *\r\n * `httpsOnly` extends the per-hop validation to the URL scheme: redirects are\r\n * followed only to `https:` targets, so an HTTPS-only caller cannot be\r\n * downgraded to plain HTTP by a 30x from the (untrusted) origin.\r\n */\r\nexport async function ssrfSafeFetch(\r\n url: string,\r\n init: RequestInit = {},\r\n options: { maxRedirects?: number; httpsOnly?: boolean } = {},\r\n): Promise<Response> {\r\n const maxRedirects = options.maxRedirects ?? 3;\r\n const dispatcher = (await createSsrfSafeDispatcher()) ?? undefined;\r\n\r\n let currentUrl = url;\r\n for (let hop = 0; hop <= maxRedirects; hop++) {\r\n if (options.httpsOnly && new URL(currentUrl).protocol !== \"https:\") {\r\n throw new Error(\r\n `SSRF blocked: refusing to fetch non-HTTPS address (${currentUrl})`,\r\n );\r\n }\r\n if (await isBlockedExtensionUrlWithDns(currentUrl)) {\r\n throw new Error(\r\n `SSRF blocked: refusing to fetch private/internal address (${currentUrl})`,\r\n );\r\n }\r\n const fetchOpts: RequestInit & { dispatcher?: unknown } = {\r\n ...init,\r\n redirect: \"manual\",\r\n };\r\n if (dispatcher) fetchOpts.dispatcher = dispatcher;\r\n\r\n const response = await fetch(currentUrl, fetchOpts);\r\n if (response.status >= 300 && response.status < 400) {\r\n const location = response.headers.get(\"location\");\r\n if (!location) return response;\r\n // Drain the redirect body so the hop's connection is released instead\r\n // of being held until GC.\r\n await response.body?.cancel().catch(() => {});\r\n currentUrl = new URL(location, currentUrl).href;\r\n continue;\r\n }\r\n return response;\r\n }\r\n throw new Error(\r\n `SSRF blocked: too many redirects (>${maxRedirects}) while fetching ${url}`,\r\n );\r\n}\r\n\r\n// ─────────────────────────────────────────────────────────────────────────────\r\n// Legacy aliases — predate the Tools → Extensions rename. Templates import\r\n// these via the legacy `@agent-native/core/tools/url-safety` subpath; keep\r\n// the names exported so they keep resolving until every consumer updates.\r\n// ─────────────────────────────────────────────────────────────────────────────\r\n\r\nexport { isBlockedExtensionUrl as isBlockedToolUrl };\r\nexport { isBlockedExtensionUrlWithDns as isBlockedToolUrlWithDns };\r\nexport { ssrfSafeFetch as ssrfSafeToolFetch };\r\n"]}
|
|
1
|
+
{"version":3,"file":"url-safety.js","sourceRoot":"","sources":["../../src/extensions/url-safety.ts"],"names":[],"mappings":"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","sourcesContent":["const METADATA_HOSTS = [\n \"metadata.google.internal\",\n \"metadata.google.internal.\",\n];\n\nconst DNS_REBIND_SUFFIXES = [\n \".nip.io\",\n \".sslip.io\",\n \".xip.io\",\n \".localtest.me\",\n \".lvh.me\",\n];\n\nfunction isPrivateIpv4(a: number, b: number, c = 0, d = 0): boolean {\n if (![a, b, c, d].every((part) => part >= 0 && part <= 255)) return true;\n if (a === 127) return true;\n if (a === 10) return true;\n if (a === 172 && b >= 16 && b <= 31) return true;\n if (a === 192 && b === 168) return true;\n if (a === 169 && b === 254) return true;\n if (a === 0) return true;\n if (a === 100 && b >= 64 && b <= 127) return true;\n if (a === 192 && b === 0) return true;\n if (a === 198 && (b === 18 || b === 19)) return true;\n if (a === 192 && b === 0 && c === 2) return true;\n if (a === 198 && b === 51 && c === 100) return true;\n if (a === 203 && b === 0 && c === 113) return true;\n if (a >= 224) return true;\n return false;\n}\n\nfunction isPrivateIpv4MappedHex(host: string): boolean {\n const mapped = host.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);\n if (!mapped) return false;\n const high = Number.parseInt(mapped[1], 16);\n const low = Number.parseInt(mapped[2], 16);\n if (high < 0 || high > 0xffff || low < 0 || low > 0xffff) return false;\n const a = (high >> 8) & 0xff;\n const b = high & 0xff;\n const c = (low >> 8) & 0xff;\n const d = low & 0xff;\n return isPrivateIpv4(a, b, c, d);\n}\n\nfunction isPrivateHost(hostname: string): boolean {\n const host = hostname.toLowerCase().replace(/^\\[|\\]$/g, \"\");\n if (\n host === \"localhost\" ||\n host === \"::1\" ||\n host === \"::0\" ||\n host === \"::\"\n ) {\n return true;\n }\n if (METADATA_HOSTS.includes(host)) return true;\n\n // IPv6 ULA/link-local/multicast.\n if (/^f[cd]/.test(host) || /^fe[89ab]/.test(host)) return true;\n if (/^ff/i.test(host)) return true;\n\n // IPv4-mapped IPv6. URL parsing may preserve dotted form in some runtimes\n // or normalize it to hex, e.g. [::ffff:127.0.0.1] -> ::ffff:7f00:1.\n const v4mappedDotted = host.match(/^::ffff:(\\d+\\.\\d+\\.\\d+\\.\\d+)$/);\n if (v4mappedDotted) {\n const [a, b, c, d] = v4mappedDotted[1].split(\".\").map(Number);\n if (isPrivateIpv4(a, b, c, d)) return true;\n }\n if (isPrivateIpv4MappedHex(host)) return true;\n\n // Dotted IPv4. URL parsing normalizes shorthand/octal/hex IPv4 forms to\n // dotted decimal before we reach this point.\n const parts = host.split(\".\");\n if (parts.length === 4 && parts.every((p) => /^\\d+$/.test(p))) {\n const [a, b, c, d] = parts.map(Number);\n if (isPrivateIpv4(a, b, c, d)) return true;\n }\n\n // Decimal integer IPv4.\n if (/^\\d+$/.test(host)) {\n const num = Number(host);\n if (num >= 0 && num <= 0xffffffff) {\n const a = (num >>> 24) & 0xff;\n const b = (num >>> 16) & 0xff;\n const c = (num >>> 8) & 0xff;\n const d = num & 0xff;\n if (isPrivateIpv4(a, b, c, d)) return true;\n }\n }\n\n return false;\n}\n\nexport function isBlockedExtensionUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n return true;\n }\n const host = parsed.hostname.toLowerCase();\n if (isPrivateHost(host)) return true;\n if (\n DNS_REBIND_SUFFIXES.some((suffix) => {\n const bare = suffix.slice(1);\n return host === bare || host.endsWith(suffix);\n })\n ) {\n return true;\n }\n } catch {\n return true;\n }\n return false;\n}\n\nfunction isIpLiteralHost(hostname: string): boolean {\n const host = hostname.toLowerCase().replace(/^\\[|\\]$/g, \"\");\n if (host.includes(\":\")) return true;\n const parts = host.split(\".\");\n return parts.length === 4 && parts.every((p) => /^\\d+$/.test(p));\n}\n\n/**\n * Async SSRF guard for environments that can resolve DNS. The synchronous\n * guard catches literals and known rebinding domains; this closes the common\n * \"public hostname resolves to a private address\" gap before dispatch.\n */\nexport async function isBlockedExtensionUrlWithDns(\n url: string,\n): Promise<boolean> {\n if (isBlockedExtensionUrl(url)) return true;\n\n let hostname: string;\n try {\n hostname = new URL(url).hostname.toLowerCase();\n } catch {\n return true;\n }\n if (!hostname || isIpLiteralHost(hostname)) return false;\n\n try {\n const { lookup } = await import(\"node:dns/promises\");\n const records = await lookup(hostname, { all: true, verbatim: true });\n return records.some((record) => isPrivateHost(record.address));\n } catch {\n // Some edge runtimes do not expose DNS lookup. Keep the deterministic\n // parser-based protections instead of failing every outbound request.\n return false;\n }\n}\n\n/**\n * Origins the deployment itself declares as its own surfaces. These come from\n * operator-controlled configuration (env), NOT from user/agent input, so a\n * fetch to them is a self-call, not an SSRF: cross-app A2A on a workspace\n * (call-agent) targets sibling apps at the deployment's own base URL, and in\n * local dev / self-hosted private networks that origin is a loopback or\n * RFC-1918 address the private-address guard would otherwise block — which\n * broke every workspace-internal agent call on `wrangler pages dev` and the\n * dev gateway. Only EXACT origin matches (scheme + host + port) are trusted;\n * everything else keeps the full SSRF policy (including redirect hops).\n */\nexport function trustedInternalOrigins(): Set<string> {\n const origins = new Set<string>();\n const addUrl = (value: unknown) => {\n if (typeof value !== \"string\" || !value.trim()) return;\n try {\n origins.add(new URL(value).origin);\n } catch {\n // Ignore malformed configured URLs — they never matched anything.\n }\n };\n const env = (globalThis as any)?.process?.env ?? {};\n addUrl(env.APP_URL);\n addUrl(env.BETTER_AUTH_URL);\n addUrl(env.WEBHOOK_BASE_URL);\n addUrl(env.WORKSPACE_GATEWAY_URL);\n const manifestJson = env.AGENT_NATIVE_WORKSPACE_APPS_JSON;\n if (typeof manifestJson === \"string\" && manifestJson.trim()) {\n try {\n const apps = JSON.parse(manifestJson);\n if (Array.isArray(apps)) {\n for (const app of apps) addUrl(app?.url);\n }\n } catch {\n // Malformed manifest — no trusted origins from it.\n }\n }\n return origins;\n}\n\n/** True when `url`'s origin exactly matches a configured internal origin. */\nexport function isTrustedInternalUrl(url: string): boolean {\n try {\n return trustedInternalOrigins().has(new URL(url).origin);\n } catch {\n return false;\n }\n}\n\n/**\n * Build an undici Dispatcher whose connect-time DNS lookup runs through a\n * private-IP guard. This closes the TOCTOU gap where:\n * 1. We resolve hostname → public IP and pass.\n * 2. Between that lookup and the actual connect, DNS rebinding flips the\n * record to a private IP.\n * 3. fetch() resolves again and connects to the private IP.\n *\n * With a custom dispatcher, the same lookup that produces the IP also gates\n * the connect: if the IP is in the private set, the connect throws.\n *\n * Returns `null` if undici / node:dns are not available (e.g. some edge\n * runtimes); the caller should fall back to the regular `fetch` path —\n * `isBlockedExtensionUrlWithDns` will still have caught most rebinding cases.\n */\nexport async function createSsrfSafeDispatcher(): Promise<unknown | null> {\n // Keep the optional undici import opaque to Vite/Rolldown. A static\n // `import(\"undici\")` makes browser builds try to resolve and bundle undici\n // even though this dispatcher is only useful in Node server runtimes.\n let undici: any;\n let dnsModule: any;\n try {\n const runtimeImport = new Function(\n \"specifier\",\n \"return import(specifier)\",\n ) as (specifier: string) => Promise<any>;\n undici = await runtimeImport(\"undici\");\n dnsModule = await import(\"node:dns\");\n } catch {\n return null;\n }\n\n const { Agent } = undici;\n const { lookup } = dnsModule;\n if (!Agent || !lookup) return null;\n\n return new Agent({\n connect: {\n // Override DNS lookup at connect time so the IP we hand to undici's\n // socket is the one we authorized. Reject any record in the private\n // set BEFORE the TCP handshake.\n lookup: (\n hostname: string,\n options: any,\n callback: (\n err: NodeJS.ErrnoException | null,\n address?: string | { address: string; family: number }[],\n family?: number,\n ) => void,\n ) => {\n lookup(\n hostname,\n { all: true, verbatim: true },\n (err: NodeJS.ErrnoException | null, addresses: any) => {\n if (err) return callback(err);\n const list: { address: string; family: number }[] = Array.isArray(\n addresses,\n )\n ? addresses\n : [{ address: addresses, family: 4 }];\n for (const record of list) {\n if (isPrivateHost(record.address)) {\n const e = new Error(\n `Connect blocked: ${hostname} resolved to private address ${record.address}`,\n ) as NodeJS.ErrnoException;\n e.code = \"EAI_BLOCKED\";\n return callback(e);\n }\n }\n // Mirror Node's lookup behavior: when `all` is true, return the\n // array; otherwise the first entry. undici's connect honors\n // `options.all`.\n if (options && options.all) {\n return callback(null, list as any);\n }\n const first = list[0];\n return callback(null, first.address, first.family);\n },\n );\n },\n },\n });\n}\n\n/**\n * SSRF-safe `fetch` for any server-side request to a user/agent-supplied URL.\n *\n * Applies the same protections the extension proxy uses, so every call site\n * that fetches an untrusted URL gets them without re-implementing the loop:\n * 1. Pre-flight DNS-aware private-address check (isBlockedExtensionUrlWithDns)\n * on the initial URL and on every redirect hop.\n * 2. A connect-time dispatcher that re-checks the resolved IP at TCP-connect\n * time (closes the DNS-rebinding TOCTOU) when undici is available.\n * 3. Manual redirect handling — a public URL cannot 30x-redirect into the\n * private network because each hop is re-validated before it is followed.\n *\n * Throws an Error whose message starts with \"SSRF blocked:\" when a target\n * (initial or via redirect) resolves to a private/internal address, or when the\n * redirect limit is exceeded. Otherwise returns the final Response.\n *\n * `httpsOnly` extends the per-hop validation to the URL scheme: redirects are\n * followed only to `https:` targets, so an HTTPS-only caller cannot be\n * downgraded to plain HTTP by a 30x from the (untrusted) origin.\n */\nexport async function ssrfSafeFetch(\n url: string,\n init: RequestInit = {},\n options: { maxRedirects?: number; httpsOnly?: boolean } = {},\n): Promise<Response> {\n const maxRedirects = options.maxRedirects ?? 3;\n const dispatcher = (await createSsrfSafeDispatcher()) ?? undefined;\n\n let currentUrl = url;\n for (let hop = 0; hop <= maxRedirects; hop++) {\n if (options.httpsOnly && new URL(currentUrl).protocol !== \"https:\") {\n throw new Error(\n `SSRF blocked: refusing to fetch non-HTTPS address (${currentUrl})`,\n );\n }\n // The deployment's own configured origins are self-calls (workspace A2A,\n // self-dispatch), not attacker-controlled targets — skip only the\n // private-address block for exact origin matches. Each redirect hop is\n // still re-validated, so a trusted origin cannot 30x into the private\n // network.\n const trustedHop = isTrustedInternalUrl(currentUrl);\n if (!trustedHop && (await isBlockedExtensionUrlWithDns(currentUrl))) {\n throw new Error(\n `SSRF blocked: refusing to fetch private/internal address (${currentUrl})`,\n );\n }\n const fetchOpts: RequestInit & { dispatcher?: unknown } = {\n ...init,\n redirect: \"manual\",\n };\n // The connect-time guard would also reject a trusted internal origin's\n // loopback/private IP at the TCP layer — attach it only to untrusted hops.\n if (dispatcher && !trustedHop) fetchOpts.dispatcher = dispatcher;\n\n const response = await fetch(currentUrl, fetchOpts);\n if (response.status >= 300 && response.status < 400) {\n const location = response.headers.get(\"location\");\n if (!location) return response;\n // Drain the redirect body so the hop's connection is released instead\n // of being held until GC.\n await response.body?.cancel().catch(() => {});\n currentUrl = new URL(location, currentUrl).href;\n continue;\n }\n return response;\n }\n throw new Error(\n `SSRF blocked: too many redirects (>${maxRedirects}) while fetching ${url}`,\n );\n}\n\n// ─────────────────────────────────────────────────────────────────────────────\n// Legacy aliases — predate the Tools → Extensions rename. Templates import\n// these via the legacy `@agent-native/core/tools/url-safety` subpath; keep\n// the names exported so they keep resolving until every consumer updates.\n// ─────────────────────────────────────────────────────────────────────────────\n\nexport { isBlockedExtensionUrl as isBlockedToolUrl };\nexport { isBlockedExtensionUrlWithDns as isBlockedToolUrlWithDns };\nexport { ssrfSafeFetch as ssrfSafeToolFetch };\n"]}
|
|
@@ -17,12 +17,12 @@ declare const _default: import("../../action.js").ActionDefinition<{
|
|
|
17
17
|
id?: undefined;
|
|
18
18
|
provider?: undefined;
|
|
19
19
|
} | {
|
|
20
|
-
error?: undefined;
|
|
21
20
|
configured?: undefined;
|
|
22
21
|
connectPath?: undefined;
|
|
23
22
|
url: string;
|
|
24
23
|
id: string;
|
|
25
24
|
provider: string;
|
|
25
|
+
error?: undefined;
|
|
26
26
|
}>;
|
|
27
27
|
export default _default;
|
|
28
28
|
//# sourceMappingURL=upload-image.d.ts.map
|
|
@@ -41,16 +41,16 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
41
41
|
thumbsUpRate: number;
|
|
42
42
|
avgEvalScore: number;
|
|
43
43
|
} | {
|
|
44
|
-
error?: undefined;
|
|
45
44
|
summary: import("./types.js").TraceSummary;
|
|
46
45
|
spans: import("./types.js").TraceSpan[];
|
|
47
46
|
id?: undefined;
|
|
47
|
+
error?: undefined;
|
|
48
48
|
ok?: undefined;
|
|
49
49
|
} | {
|
|
50
|
-
error?: undefined;
|
|
51
50
|
summary?: undefined;
|
|
52
51
|
spans?: undefined;
|
|
53
52
|
id: string;
|
|
53
|
+
error?: undefined;
|
|
54
54
|
ok?: undefined;
|
|
55
55
|
} | {
|
|
56
56
|
summary?: undefined;
|
|
@@ -59,10 +59,10 @@ export declare function createObservabilityHandler(): import("h3").EventHandlerW
|
|
|
59
59
|
error: any;
|
|
60
60
|
ok?: undefined;
|
|
61
61
|
} | {
|
|
62
|
-
error?: undefined;
|
|
63
62
|
summary?: undefined;
|
|
64
63
|
spans?: undefined;
|
|
65
64
|
id?: undefined;
|
|
65
|
+
error?: undefined;
|
|
66
66
|
ok: boolean;
|
|
67
67
|
}>>;
|
|
68
68
|
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -52,8 +52,8 @@ export declare function handleDeleteResource(event: any): Promise<{
|
|
|
52
52
|
error: string;
|
|
53
53
|
ok?: undefined;
|
|
54
54
|
} | {
|
|
55
|
-
error?: undefined;
|
|
56
55
|
ok: boolean;
|
|
56
|
+
error?: undefined;
|
|
57
57
|
}>;
|
|
58
58
|
/** POST /_agent-native/resources/upload — upload a file as a resource */
|
|
59
59
|
export declare function handleUploadResource(event: any): Promise<import("./store.js").Resource | {
|
|
@@ -74,10 +74,10 @@ export declare function handleUploadResource(event: any): Promise<import("./stor
|
|
|
74
74
|
runId: string | null;
|
|
75
75
|
expiresAt: number | null;
|
|
76
76
|
metadata: string | null;
|
|
77
|
-
error?: undefined;
|
|
78
77
|
url: string;
|
|
79
78
|
provider: string;
|
|
80
79
|
storageSetupRequired?: undefined;
|
|
80
|
+
error?: undefined;
|
|
81
81
|
} | {
|
|
82
82
|
error: string;
|
|
83
83
|
storageSetupRequired: boolean;
|
|
@@ -27,11 +27,11 @@ export declare function resolveAgentEngineApiKeyWriteTarget(event: H3Event, scop
|
|
|
27
27
|
export declare function createAgentEngineApiKeyHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
28
28
|
error: any;
|
|
29
29
|
} | {
|
|
30
|
-
error?: undefined;
|
|
31
30
|
ok: boolean;
|
|
32
31
|
key: string;
|
|
33
32
|
baseUrlKey?: string;
|
|
34
33
|
scope: AgentEngineApiKeyScope;
|
|
34
|
+
error?: undefined;
|
|
35
35
|
}>>;
|
|
36
36
|
export {};
|
|
37
37
|
//# sourceMappingURL=agent-engine-api-key-route.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jami-studio/core",
|
|
3
|
-
"version": "0.92.
|
|
3
|
+
"version": "0.92.26",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/studio-jami/jami-studio#readme",
|
|
6
6
|
"bugs": {
|