Skip to content

UnapprovedPathMode has loopholes #23659

@greeble-dev

Description

@greeble-dev

Bevy version and features

53ddd5e (2026/04/03)

Problem

The asset system can deny access to absolute paths, and relative paths containing .. and .. See UnapprovedPathMode

This works for AssetServer::load and similar - they end up calling AssetServer::load_with_meta_transform which does a AssetPath::is_unapproved check (source)

But there's a bunch of load functions that don't do any checks:

  • AssetServer::load_erased
  • AssetServer::load_untyped
  • AssetServer::load_untyped_async
  • AssetServer::load_unknown_type_with_meta_transform
  • LoadContext::read_asset_bytes

There's also problems with NestedLoader, which is used to load assets within loaders. First, it doesn't inherit the override flag from load_override and similar, so UnapprovedPathMode::Deny is broken - loads will be falsely rejected. Second, various paths either have no checks or incorrect checks:

  • Deferred:
    • should_load_dependencies = true,
      • Static typed.
        • Forbid works, but Deny is broken as NestedLoader<'_, '_, StaticTyped, Deferred>::load calls load_with_meta_transform with override_unapproved hard coded to true.
      • Dynamic and unknown typed.
        • Calls AssetServer::load_erased_with_meta_transform/load_unknown_type_with_meta_transform, which should do the check (but currently doesn't).
    • should_load_dependencies = false.
      • Calls get_or_create_path_handle which doesn't do any checks.
  • Immediate:
    • No checks.

Solution?

I started working on some unit tests and fixes - branch here. But I've hit pause due to some issues.

First, while most of the problems can be solved with more plumbing and checks, I'm not so sure about load_override semantics and NestedLoader. Is it actually correct to inherit the parent override? Is this transitive to all dependencies and immediate loads? How does this get routed through the system?

Second, maybe the checks are in the wrong place? Currently they're in AssetServer. But what if the checks were in AssetSource instead? So AssetPlugin::unapproved_path_mode would go away - instead individual asset sources would have their own parameter.

This has some upsides:

  • Somewhat centralizes the checks.
    • They do get duplicated across sources, but it's harder to miss a check elsewhere since almost everything goes through the source eventually.
    • One flaw is get_or_create_path_handle, which doesn't go through the source.
  • Makes the AssetServer less dependent on Path and OS specific quirks.
  • Different sources can have different settings.
    • Not sure if this is actually a good thing, but worth exploring?
    • Might also remove the need for UnapprovedPathMode::Deny and the NestedLoader override inheritance.
      • The user can map the same path as two different sources with different approvals.
      • So instead of calling load_override("path") they call load("my_unapproved_source://path").

The big downside is ergonomics. Instead of setting a single AssetPlugin::unapproved_path_mode, the user has to set it on each source.

So I'm not sure about solutions, but I can make a PR with failing unit tests if desired.

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-AssetsLoad files from disk to use for things like images, models, and soundsC-BugAn unexpected or incorrect behaviorS-Needs-DesignThis issue requires design work to think about how it would best be accomplished

    Type

    No type

    Projects

    Status

    Needs SME Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions