heygen-com/hyperframes · error · Error
freeze failed: refusing non-figma url ${url} (https + figma
Error message
freeze failed: refusing non-figma url ${url} (https + figma hosts only) What it means
Thrown by freezeUrl when isAllowedFreezeUrl(url) returns false. The allowlist requires https AND a host that is exactly 'figma.com', ends with '.figma.com', or ends with '.amazonaws.com' (figma's S3 CDN). This is an SSRF guard: a crafted manifest or config URL pointing at internal metadata endpoints (169.254.169.254, localhost services, etc.) must not be fetched and written to disk by the freeze step. Non-https and non-figma hosts are both rejected.
Source
Thrown at packages/core/src/figma/freeze.ts:52
* Only figma-owned hosts may be frozen from a URL — render/CDN responses
* come from figma.com subdomains or figma's S3 buckets. Blocks SSRF via a
* crafted manifest/config URL (metadata endpoints, internal services).
*/
export function isAllowedFreezeUrl(url: string): boolean {
let parsed: URL;
try {
parsed = new URL(url);
} catch {
return false;
}
if (parsed.protocol !== "https:") return false;
const host = parsed.hostname;
return host === "figma.com" || host.endsWith(".figma.com") || host.endsWith(".amazonaws.com");
}
export async function freezeUrl(url: string, destPath: string): Promise<number> {
if (!isAllowedFreezeUrl(url))
throw new Error(`freeze failed: refusing non-figma url ${url} (https + figma hosts only)`);
const res = await fetch(url);
if (!res.ok) throw new Error(`freeze failed: HTTP ${res.status}`);
const declared = Number(res.headers.get("content-length") ?? 0);
if (exceedsFreezeCap(declared))
throw new Error(`freeze failed: content-length ${declared} exceeds ${MAX_FREEZE_BYTES} cap`);
return freezeBytes(new Uint8Array(await res.arrayBuffer()), destPath);
}
export function freezeLocalFile(srcPath: string, destPath: string): void {
const size = statSync(srcPath).size;
if (exceedsFreezeCap(size))
throw new Error(`freeze failed: ${size} bytes exceeds ${MAX_FREEZE_BYTES} cap`);
mkdirSync(dirname(destPath), { recursive: true });
copyFileSync(srcPath, destPath);
}
View on GitHub (pinned to c2996c8626)
Solutions
- Use the original figma CDN URL returned by renderNodes/imageFills — do not rewrite it.
- If behind a proxy that rewrites hosts, configure the proxy to preserve the figma host or bypass it for figma domains.
- For local assets, use freezeLocalFile instead of freezeUrl.
- Confirm the URL starts with https:// and the host is figma.com / a *.figma.com subdomain / *.amazonaws.com.
Example fix
// before — proxy rewrote the host, SSRF guard rejects
await freezeUrl('https://cdn.corp-proxy.internal/asset.png', dest);
// after — pass the original figma CDN url through unchanged
await freezeUrl('https://s3.us-east-1.amazonaws.com/figma/...', dest); Defensive patterns
Strategy: validation
Validate before calling
import { isAllowedFreezeUrl } from '.../figma/freeze';
export function assertFreezable(url: string): void {
if (!isAllowedFreezeUrl(url)) {
throw new Error(`${url} is not a figma CDN URL — refusing to fetch`);
}
}
assertFreezable(url);
await freezeUrl(url, dest); Try / catch
try {
await freezeUrl(url, dest);
} catch (err) {
if (err instanceof Error && /refusing non-figma url/.test(err.message)) {
// log a security warning, do NOT bypass
} else throw err;
} Prevention
- Never rewrite figma CDN URLs through a non-figma proxy before freezing.
- For local assets use freezeLocalFile, not freezeUrl with a file:// or localhost URL.
- Treat this error as a security signal — investigate how the non-figma URL entered the manifest.
When it happens
Trigger: Passing a http:// URL; an http://localhost internal service URL; an https://evil.com URL planted in a manifest; an https:// internal metadata endpoint; a URL whose host is a bare IP (never matches the allowlist).
Common situations: A figma file's image fill URL was rewritten by a corporate proxy to a non-figma host; a dev pointing freezeUrl at a locally-hosted mirror for testing; a maliciously crafted figma manifest (the threat model this guard exists for).
Related errors
- freeze failed: empty bytes
- freeze failed: ${bytes.length} bytes exceeds ${MAX_FREEZE_BY
- freeze failed: HTTP ${res.status}
- freeze failed: content-length ${declared} exceeds ${MAX_FREE
- freeze failed: ${size} bytes exceeds ${MAX_FREEZE_BYTES} cap
AI-assisted analysis of heygen-com/hyperframes@c2996c8626 (2026-08-12).
Data as JSON: /api/errors/bbcd462b8a8e4218.
Report an issue: GitHub.