mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(cli): re-validate SSRF denylist on redirects + harden isPrivateUrl (#1212)
## Summary - Adds `safeFetch`, a redirect-aware wrapper around `fetch` that re-runs the SSRF denylist on every hop before following a redirect. - Routes `fetchBuffer` and the Lottie media fetch through `safeFetch` so redirect chains can't bounce through a public URL to reach an internal or cloud-metadata host. - Hardens `isPrivateUrl` to also block `0.0.0.0` / `0.0.0.0/8`, IPv6 loopback (`::1`), IPv4-mapped (`::ffff:…`), unique-local (`fc00::/7`), and link-local (`fe80::/10`) ranges. ## Security **F-002 MED** — `fetchBuffer` followed redirects without re-checking the denylist on the destination. A `30x` redirect from an allowlisted public URL to `169.254.169.254` or an internal host would succeed, leaking the response to the caller (e.g. captured page assets written to local disk). **F-003 MED** — `isPrivateUrl` did not cover `0.0.0.0` (maps to localhost on most OSes), IPv6 loopback, or IPv6 private ranges. An asset URL using those addresses would bypass the denylist. Alternate IPv4 encodings (decimal/octal/hex) are already normalized to dotted-quad by WHATWG URL parsing and remain blocked. ## Test plan - [x] Unit tests cover redirect-chain blocking (redirect to metadata IP rejected) - [x] Unit tests cover new `isPrivateUrl` address forms (`0.0.0.0`, `::1`, `fc00::1`, `fe80::1`, `::ffff:192.168.1.1`) - [x] Existing fetch and asset-download tests pass
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
import type { Browser, Page } from "puppeteer-core";
|
||||
import { mkdirSync, writeFileSync, readdirSync, readFileSync, statSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { isPrivateUrl } from "./assetDownloader.js";
|
||||
import { safeFetch } from "./assetDownloader.js";
|
||||
|
||||
/** Discovered Lottie item from network interception or DOM scan. */
|
||||
export interface DiscoveredLottie {
|
||||
@@ -42,14 +42,12 @@ export async function saveLottieAnimations(
|
||||
// Already have the JSON data from network interception
|
||||
jsonData = JSON.stringify(lottieItem.data);
|
||||
} else if (lottieItem.url) {
|
||||
// SSRF guard — don't fetch private/internal URLs
|
||||
if (isPrivateUrl(lottieItem.url)) continue;
|
||||
// Download the file
|
||||
const res = await fetch(lottieItem.url, {
|
||||
// SSRF guard — safeFetch re-checks the denylist on every redirect hop
|
||||
const res = await safeFetch(lottieItem.url, {
|
||||
signal: AbortSignal.timeout(10000),
|
||||
headers: { "User-Agent": "HyperFrames/1.0" },
|
||||
});
|
||||
if (!res.ok) continue;
|
||||
if (!res || !res.ok) continue;
|
||||
const buf = Buffer.from(await res.arrayBuffer());
|
||||
|
||||
if (lottieItem.url.endsWith(".lottie")) {
|
||||
|
||||
Reference in New Issue
Block a user