SIGN IN SIGN UP

fix(producer): localize remote @font-face src URLs before render (#1155)

* fix(producer): localize remote @font-face src URLs before render

Remote font URLs in @font-face blocks fail with a CORS rejection when
the renderer fetches them from http://localhost:PORT (S3 does not echo
the local origin in Access-Control-Allow-Origin). Chrome falls back to
the next font in the stack (e.g. Arial), producing wrong typography.

localizeRemoteFontFaces() scans <style> blocks, extracts HTTP url()
references inside @font-face rules, downloads them in parallel into
_remote_media/, and rewrites the CSS url() references to local paths —
the same pattern as localizeRemoteMediaSources() for <video>/<audio>.

Background url() references outside @font-face blocks are intentionally
left untouched to avoid downloading arbitrary images.

The shared download+rewrite logic is extracted into downloadAndRewriteUrls()
to eliminate duplication between the two localize functions.

Reported via the Beasty Style caption template (Komika Axis .ttf from S3
falling back to Arial on every cloud render).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(engine): add SSRF guard to downloadToTemp (blocks private/IMDS addresses)

Customer-supplied compositions can author @font-face src URLs (and <video>/
<audio> src attrs via the existing localize path) that point to private
infrastructure. Without a guard, the producer's downloadToTemp would fetch
http://169.254.169.254/... (AWS IMDS), RFC1918, loopback, etc., save the
response to _remote_media/, and expose it via the local file server.

assertPublicHttpsUrl() rejects:
  - Non-HTTPS (http://) — all composition fetches must use HTTPS
  - 169.254.x (AWS link-local / IMDS)
  - 127.x / localhost / 0.x (loopback / unspecified)
  - 10.x, 172.16–172.31, 192.168.x (RFC1918)
  - [::1], [fc...], [fd...] (IPv6 loopback + unique-local)

The guard fires before the cache check so a blocked URL never gets into
the in-flight map. Applies to both the font-face localize path (PR #1155)
and the existing video/audio localize path (PR #1146) since both call
downloadToTemp.

Note: DNS-rebinding bypasses are not closed by this check (hostname
comparison only, no DNS resolution). Acceptable risk for current threat
model; server-side DNS validation can be layered on later.

12 unit tests covering all blocked ranges + the allowed edge cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(engine): fix TypeScript strict-mode error in urlDownloader SSRF guard

m[1] from RegExp.match() is typed string | undefined; parseInt requires string.
Use nullish coalescing to satisfy tsc without changing runtime behavior —
the regex guarantees m[1] is always defined when the match succeeds.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(engine): use vitest import in urlDownloader test

bun:test is not available in CI — the engine package runs tests via vitest.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
M
Miguel Ángel committed
b1b03782a1364f160b5504d46ad36cef277cca91
Parent: bda4a32
Committed by GitHub <noreply@github.com> on 6/2/2026, 12:12:53 AM