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

  1. Use the original figma CDN URL returned by renderNodes/imageFills — do not rewrite it.
  2. If behind a proxy that rewrites hosts, configure the proxy to preserve the figma host or bypass it for figma domains.
  3. For local assets, use freezeLocalFile instead of freezeUrl.
  4. 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

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


AI-assisted analysis of heygen-com/hyperframes@c2996c8626 (2026-08-12). Data as JSON: /api/errors/bbcd462b8a8e4218. Report an issue: GitHub.