Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AssetServer out of bounds protection #18088

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
121 changes: 120 additions & 1 deletion crates/bevy_asset/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,33 @@ pub struct AssetPlugin {
pub mode: AssetMode,
/// How/If asset meta files should be checked.
pub meta_check: AssetMetaCheck,
/// How to handle load requests of files that are outside the approved directories.
///
/// Approved folders are [`AssetPlugin::file_path`] and the folder of each
/// [`AssetSource`](io::AssetSource). Subfolders within these folders are also valid.
pub unapproved_path_mode: UnapprovedPathMode,
}

/// Determines how to react to attempts to load assets not inside the approved folders.
///
/// Approved folders are [`AssetPlugin::file_path`] and the folder of each
/// [`AssetSource`](io::AssetSource). Subfolders within these folders are also valid.
///
/// It is strongly discouraged to use [`Allow`](UnapprovedPathMode::Allow) if your
/// app will include scripts or modding support, as it could allow allow arbitrary file
/// access for malicious code.
///
/// See [`AssetPath::is_unapproved`](crate::AssetPath::is_unapproved)
#[derive(Clone, Default)]
pub enum UnapprovedPathMode {
/// Unapproved asset loading is allowed. This is strongly discouraged.
Allow,
/// Fails to load any asset that is is unapproved, unless an override method is used, like
/// [`AssetServer::load_override`].
Deny,
/// Fails to load any asset that is is unapproved.
#[default]
Forbid,
}

/// Controls whether or not assets are pre-processed before being loaded.
Expand Down Expand Up @@ -311,6 +338,7 @@ impl Default for AssetPlugin {
processed_file_path: Self::DEFAULT_PROCESSED_FILE_PATH.to_string(),
watch_for_changes_override: None,
meta_check: AssetMetaCheck::default(),
unapproved_path_mode: UnapprovedPathMode::default(),
}
}
}
Expand Down Expand Up @@ -351,6 +379,7 @@ impl Plugin for AssetPlugin {
AssetServerMode::Unprocessed,
self.meta_check.clone(),
watch,
self.unapproved_path_mode.clone(),
));
}
AssetMode::Processed => {
Expand All @@ -367,6 +396,7 @@ impl Plugin for AssetPlugin {
AssetServerMode::Processed,
AssetMetaCheck::Always,
watch,
self.unapproved_path_mode.clone(),
))
.insert_resource(processor)
.add_systems(bevy_app::Startup, AssetProcessor::start);
Expand All @@ -380,6 +410,7 @@ impl Plugin for AssetPlugin {
AssetServerMode::Processed,
AssetMetaCheck::Always,
watch,
self.unapproved_path_mode.clone(),
));
}
}
Expand Down Expand Up @@ -639,7 +670,7 @@ mod tests {
},
loader::{AssetLoader, LoadContext},
Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetPath,
AssetPlugin, AssetServer, Assets, DuplicateLabelAssetError, LoadState,
AssetPlugin, AssetServer, Assets, DuplicateLabelAssetError, LoadState, UnapprovedPathMode,
};
use alloc::{
boxed::Box,
Expand Down Expand Up @@ -1856,4 +1887,92 @@ mod tests {

#[derive(Asset, TypePath)]
pub struct TupleTestAsset(#[dependency] Handle<TestAsset>);

fn unapproved_path_setup(mode: UnapprovedPathMode) -> App {
let dir = Dir::default();
let a_path = "../a.cool.ron";
let a_ron = r#"
(
text: "a",
dependencies: [],
embedded_dependencies: [],
sub_texts: [],
)"#;

dir.insert_asset_text(Path::new(a_path), a_ron);

let mut app = App::new();
let memory_reader = MemoryAssetReader { root: dir };
app.register_asset_source(
AssetSourceId::Default,
AssetSource::build().with_reader(move || Box::new(memory_reader.clone())),
)
.add_plugins((
TaskPoolPlugin::default(),
LogPlugin::default(),
AssetPlugin {
unapproved_path_mode: mode,
..Default::default()
},
));
app.init_asset::<CoolText>();

app
}

fn load_a_asset(assets: Res<AssetServer>) {
let a = assets.load::<CoolText>("../a.cool.ron");
if a == Handle::default() {
panic!()
}
}

fn load_a_asset_override(assets: Res<AssetServer>) {
let a = assets.load_override::<CoolText>("../a.cool.ron");
if a == Handle::default() {
panic!()
}
}

#[test]
#[should_panic]
fn unapproved_path_forbid_should_panic() {
let mut app = unapproved_path_setup(UnapprovedPathMode::Forbid);

fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
app.add_systems(Update, (uses_assets, load_a_asset_override));

app.world_mut().run_schedule(Update);
}

#[test]
#[should_panic]
fn unapproved_path_deny_should_panic() {
let mut app = unapproved_path_setup(UnapprovedPathMode::Deny);

fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
app.add_systems(Update, (uses_assets, load_a_asset));

app.world_mut().run_schedule(Update);
}

#[test]
fn unapproved_path_deny_should_finish() {
let mut app = unapproved_path_setup(UnapprovedPathMode::Deny);

fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
app.add_systems(Update, (uses_assets, load_a_asset_override));

app.world_mut().run_schedule(Update);
}

#[test]
fn unapproved_path_allow_should_finish() {
let mut app = unapproved_path_setup(UnapprovedPathMode::Allow);

fn uses_assets(_asset: ResMut<Assets<CoolText>>) {}
app.add_systems(Update, (uses_assets, load_a_asset));

app.world_mut().run_schedule(Update);
}
}
9 changes: 6 additions & 3 deletions crates/bevy_asset/src/loader_builders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -305,9 +305,12 @@ impl NestedLoader<'_, '_, StaticTyped, Deferred> {
pub fn load<'c, A: Asset>(self, path: impl Into<AssetPath<'c>>) -> Handle<A> {
let path = path.into().to_owned();
let handle = if self.load_context.should_load_dependencies {
self.load_context
.asset_server
.load_with_meta_transform(path, self.meta_transform, ())
self.load_context.asset_server.load_with_meta_transform(
path,
self.meta_transform,
(),
true,
)
} else {
self.load_context
.asset_server
Expand Down
45 changes: 45 additions & 0 deletions crates/bevy_asset/src/path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,51 @@ impl<'a> AssetPath<'a> {
}
})
}

/// Returns `true` if this [`AssetPath`] points to a file that is
/// outside of it's [`AssetSource`](crate::io::AssetSource) folder.
///
/// ## Example
/// ```
/// # use bevy_asset::AssetPath;
/// // Inside the default AssetSource.
/// let path = AssetPath::parse("thingy.png");
/// assert!( ! path.is_unapproved());
/// let path = AssetPath::parse("gui/thingy.png");
/// assert!( ! path.is_unapproved());
///
/// // Inside a different AssetSource.
/// let path = AssetPath::parse("embedded://thingy.png");
/// assert!( ! path.is_unapproved());
///
/// // Exits the `AssetSource`s directory.
/// let path = AssetPath::parse("../thingy.png");
/// assert!(path.is_unapproved());
/// let path = AssetPath::parse("folder/../../thingy.png");
/// assert!(path.is_unapproved());
///
/// // This references the linux root directory.
/// let path = AssetPath::parse("/home/thingy.png");
/// assert!(path.is_unapproved());
/// ```
pub fn is_unapproved(&self) -> bool {
use std::path::Component;
let mut simplified = PathBuf::new();
for component in self.path.components() {
match component {
Component::Prefix(_) | Component::RootDir => return true,
Component::CurDir => {}
Component::ParentDir => {
if !simplified.pop() {
return true;
}
}
Component::Normal(os_str) => simplified.push(os_str),
}
}

false
}
}

impl AssetPath<'static> {
Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_asset/src/processor/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ use crate::{
AssetMetaDyn, AssetMetaMinimal, ProcessedInfo, ProcessedInfoMinimal,
},
AssetLoadError, AssetMetaCheck, AssetPath, AssetServer, AssetServerMode, DeserializeMetaError,
MissingAssetLoaderForExtensionError,
MissingAssetLoaderForExtensionError, UnapprovedPathMode,
};
use alloc::{borrow::ToOwned, boxed::Box, collections::VecDeque, sync::Arc, vec, vec::Vec};
use bevy_ecs::prelude::*;
Expand Down Expand Up @@ -122,6 +122,7 @@ impl AssetProcessor {
AssetServerMode::Processed,
AssetMetaCheck::Always,
false,
UnapprovedPathMode::default(),
);
Self { server, data }
}
Expand Down
Loading