Skip to content

fix(fonts): deny a Google Fonts fetch unless it is opted into - #420

Merged
LeadcodeDev merged 1 commit into
mainfrom
fix/gate-google-font-fetch
Sep 29, 2026
Merged

LeadcodeDev merged 1 commit into
mainfrom
fix/gate-google-font-fetch

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Closes #320.

The defect

A scenario declaring fonts: [{ source: "google", ... }] made validate and
still reach fonts.googleapis.com and write the result into the font cache.
Neither command asked.

The declared consumer of a scenario is a model generating JSON, so the file is
untrusted input — and it is the file that chooses the target of the request.
Validating someone else's scenario should not be a network operation, and on a CI
runner it turns a lint step into an outbound fetch.

Shape

RemoteFontPolicy mirrors RemoteIncludePolicy rather than inventing a second
authorization vocabulary: Deny by default, one flag to opt in.
--allow-remote-fonts is global, so it works on any subcommand.

$ rustmotion validate -f scenario.json
Warning: Google Fonts: 'Zilla Slab Highlight' (weights 700) is not in the font
cache, and fetching it would reach https://fonts.googleapis.com/css2?family=Zilla
+Slab+Highlight:wght@700 — a scenario chooses that target, so it is denied by
default.
Pass --allow-remote-fonts to opt in, or place a TTF for each weight at:
  ~/.cache/rustmotion/fonts

Where the check sits, and why it matters

Immediately before the first network call, not at the top of the resolver. So it
fires exactly when a request would leave, and a family already in the cache
needs no flag at all
— an offline render that has its fonts keeps working, which
is what makes deny-by-default safe to ship. Only the weights actually missing from
the cache are named.

Verified end to end: refused without the flag (no request), fetched and cached
with it, then accepted again without it once cached.

Why a process-level policy

resolve_font_entry is reached through load_custom_fonts from eight call sites,
none of which carry a policy today. Threading a parameter through all of them buys
nothing over an atomic set once from run().

Not in scope, worth its own issue

--allow-remote-include is named in the include refusal but does not exist as a
CLI flag
. The loader hardcodes RemoteIncludePolicy::Deny, so that message
points at a flag nobody can pass. Wiring it means threading the policy through
load_scenario_from_source, which is a different change from this one.

Tests

Four, including a_family_already_in_the_cache_needs_no_flag and
only_the_weights_that_are_missing_are_named_in_the_refusal. Removing the check
fails two of them — and, during the revert, actually performs the fetch, which is
the behaviour being closed.

Gate

cargo fmt --all --check clean · cargo clippy --workspace --all-targets --features rustmotion/studio -D warnings clean · cargo test --workspace 1807 passed.

A scenario declaring fonts: [{ source: "google", ... }] made validate
and still reach fonts.googleapis.com and write the result into the font
cache. Neither command asked. The declared consumer of a scenario is a
model generating JSON, so the file is untrusted input, and it is the
file that chooses the target of the request: validating someone else's
scenario should not be a network operation, and on a CI runner it turns
a lint step into an outbound fetch.

RemoteFontPolicy mirrors RemoteIncludePolicy rather than inventing a
second authorization vocabulary: Deny by default, one flag to opt in.
--allow-remote-fonts is global, so it works on any subcommand.

The refusal sits immediately before the first network call, not at the
top of the resolver. So it fires exactly when a request would leave, and
a family already in the cache needs no flag at all -- an offline render
that has its fonts keeps working, which is what makes this safe to
default to deny. Only the weights actually missing from the cache are
named.

The policy is a process-level atomic rather than a parameter because
resolve_font_entry is reached through load_custom_fonts from eight call
sites, none of which carry a policy today.

Not in scope, and worth its own issue: --allow-remote-include is named
in the include refusal but does not exist as a CLI flag. The loader
hardcodes RemoteIncludePolicy::Deny, so the message points at a flag
nobody can pass.
@LeadcodeDev LeadcodeDev added the bug Something isn't working label Sep 29, 2026
@LeadcodeDev LeadcodeDev self-assigned this Sep 29, 2026
@LeadcodeDev
LeadcodeDev merged commit 51047ef into main Sep 29, 2026
4 checks passed
@LeadcodeDev
LeadcodeDev deleted the fix/gate-google-font-fetch branch September 29, 2026 09:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

validate and still fetch Google Fonts over the network with no opt-in

1 participant