@bacnh85/pi-web 0.10.2 → 0.10.3
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 +10 -0
- package/extensions/lib/imageapi.ts +28 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.3 (2026-09-13)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Redirect-aware SSRF guard**: image downloads now use `redirect: "manual"`
|
|
8
|
+
and re-validate every hop with the private/loopback check — a gateway URL
|
|
9
|
+
that 302s to an internal host (e.g. cloud metadata) can no longer bypass
|
|
10
|
+
the guard via fetch's default redirect following. Public redirects still
|
|
11
|
+
work (relative locations resolved per hop, max 3 hops).
|
|
12
|
+
|
|
3
13
|
## 0.10.2 (2026-09-13)
|
|
4
14
|
|
|
5
15
|
### Fixed
|
|
@@ -159,8 +159,15 @@ export function imageRateSnapshot(): Record<string, { count: number; day: string
|
|
|
159
159
|
export interface FetchLike {
|
|
160
160
|
(
|
|
161
161
|
url: string,
|
|
162
|
-
init?: { method?: string; headers?: Record<string, string>; body?: string; signal?: AbortSignal },
|
|
163
|
-
): Promise<{
|
|
162
|
+
init?: { method?: string; headers?: Record<string, string>; body?: string; signal?: AbortSignal; redirect?: string },
|
|
163
|
+
): Promise<{
|
|
164
|
+
ok: boolean;
|
|
165
|
+
status: number;
|
|
166
|
+
statusText?: string;
|
|
167
|
+
headers?: { get(name: string): string | null };
|
|
168
|
+
json(): Promise<unknown>;
|
|
169
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
170
|
+
}>;
|
|
164
171
|
}
|
|
165
172
|
|
|
166
173
|
export interface ApiImageResult {
|
|
@@ -263,17 +270,30 @@ async function downloadImage(
|
|
|
263
270
|
signal?: AbortSignal,
|
|
264
271
|
timeoutMs?: number,
|
|
265
272
|
): Promise<string> {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
273
|
+
// fetch follows redirects by default — follow manually and re-validate each
|
|
274
|
+
// hop, or a gateway URL that 302s to an internal host bypasses the guard.
|
|
275
|
+
let current = url;
|
|
276
|
+
for (let hop = 0; ; hop++) {
|
|
277
|
+
if (hop > 3) throw new Error("too many image redirects");
|
|
278
|
+
if (isLocalUrl(current)) throw new Error(`image host is private/loopback (SSRF-guarded): ${current}`);
|
|
279
|
+
const res = await raceGuard(fetchImpl(current, { method: "GET", redirect: "manual", signal }), {
|
|
280
|
+
signal,
|
|
281
|
+
timeoutMs: timeoutMs ?? 120_000,
|
|
282
|
+
label: "web_image download",
|
|
283
|
+
});
|
|
284
|
+
if ([301, 302, 303, 307, 308].includes(res.status)) {
|
|
285
|
+
const loc = res.headers?.get?.("location") ?? null;
|
|
286
|
+
if (!loc) throw new ImageApiError(res.status, `image redirect ${res.status} without a location header`);
|
|
287
|
+
current = new URL(loc, current).toString();
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (!res.ok) throw new ImageApiError(res.status, `image download failed (HTTP ${res.status})`);
|
|
272
291
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
273
292
|
if (buf.length > MAX_DOWNLOAD_BYTES) throw new Error(`image exceeds the ${MAX_DOWNLOAD_BYTES}-byte download cap`);
|
|
274
293
|
const file = path.join(outDir, `pi-web-image-${randomUUID().slice(0, 8)}-${i}${extFor(url)}`);
|
|
275
294
|
fs.writeFileSync(file, buf);
|
|
276
295
|
return file;
|
|
296
|
+
}
|
|
277
297
|
}
|
|
278
298
|
|
|
279
299
|
function extFor(url: string): string {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bacnh85/pi-web",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Pi extension for web search, page extraction, Firecrawl scraping/crawling, Crawl4AI headless browser crawling, Gemini web-tier research, free upstream image generation, and one-off gateway chat.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|