143 lines
6 KiB
Rust
143 lines
6 KiB
Rust
//! The path-escape suite for `ResourceResolver` (KERNELS.md §5.2, P0-S):
|
|
//! every way a document's resource name could reach outside its root is
|
|
//! refused, and decoder budgets refuse overflowing or oversized images
|
|
//! before anything is allocated.
|
|
use makepad_platform::resource_resolver::*;
|
|
use std::path::PathBuf;
|
|
|
|
fn fresh_dir(name: &str) -> PathBuf {
|
|
let dir = PathBuf::from(env!("CARGO_TARGET_TMPDIR")).join("resource_resolver").join(name);
|
|
let _ = std::fs::remove_dir_all(&dir);
|
|
std::fs::create_dir_all(&dir).unwrap();
|
|
dir
|
|
}
|
|
|
|
/// root/doc/{a.txt, sub/b.txt} and root/secret.txt beside the document root.
|
|
fn fixture(name: &str) -> (PathBuf, ResourceResolver) {
|
|
let base = fresh_dir(name);
|
|
let doc = base.join("doc");
|
|
std::fs::create_dir_all(doc.join("sub")).unwrap();
|
|
std::fs::write(doc.join("a.txt"), b"alpha").unwrap();
|
|
std::fs::write(doc.join("sub/b.txt"), b"beta").unwrap();
|
|
std::fs::write(base.join("secret.txt"), b"secret").unwrap();
|
|
let resolver = ResourceResolver::new(&doc).unwrap();
|
|
(base, resolver)
|
|
}
|
|
|
|
#[test]
|
|
fn plain_relative_names_resolve_beneath_the_root() {
|
|
let (_base, r) = fixture("plain");
|
|
assert_eq!(r.read("a.txt", 100).unwrap(), b"alpha");
|
|
assert_eq!(r.read("sub/b.txt", 100).unwrap(), b"beta");
|
|
assert_eq!(r.read("./sub/./b.txt", 100).unwrap(), b"beta");
|
|
assert_eq!(r.read("sub//b.txt", 100).unwrap(), b"beta");
|
|
assert!(r.resolve("sub/b.txt").unwrap().starts_with(r.root()));
|
|
}
|
|
|
|
#[test]
|
|
fn escapes_are_refused() {
|
|
let (base, r) = fixture("escapes");
|
|
let secret = base.join("secret.txt");
|
|
for name in [
|
|
"../secret.txt",
|
|
"sub/../../secret.txt",
|
|
"sub/../a.txt", // `..` is refused even when it would stay inside
|
|
"..",
|
|
secret.to_str().unwrap(),
|
|
"/etc/passwd",
|
|
"//etc/passwd",
|
|
"sub\\..\\..\\secret.txt",
|
|
"C:secret.txt",
|
|
"C:/Windows/win.ini",
|
|
"a.txt:stream",
|
|
"a.txt\0.png",
|
|
"",
|
|
".",
|
|
"./",
|
|
] {
|
|
let err = r.resolve(name).expect_err(name);
|
|
assert!(matches!(err, ResourceError::InvalidPath(_)), "{name:?}: {err}");
|
|
}
|
|
let long = "a/".repeat(600) + "x";
|
|
assert!(matches!(r.resolve(&long), Err(ResourceError::InvalidPath(_))));
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn symlinks_are_never_followed() {
|
|
let (base, r) = fixture("symlinks");
|
|
let doc = r.root().to_path_buf();
|
|
// A link to a file outside the root.
|
|
std::os::unix::fs::symlink(base.join("secret.txt"), doc.join("out.txt")).unwrap();
|
|
// A link to a directory outside the root, then a name through it.
|
|
std::os::unix::fs::symlink(&base, doc.join("up")).unwrap();
|
|
// A link that stays inside the root is refused too: links are not
|
|
// followed at all, so no check depends on where they point.
|
|
std::os::unix::fs::symlink(doc.join("a.txt"), doc.join("alias.txt")).unwrap();
|
|
std::os::unix::fs::symlink(doc.join("sub"), doc.join("subalias")).unwrap();
|
|
for name in ["out.txt", "up/secret.txt", "up", "alias.txt", "subalias/b.txt"] {
|
|
let err = r.read(name, 100).expect_err(name);
|
|
assert!(matches!(err, ResourceError::Symlink(_)), "{name}: {err}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn missing_files_directories_and_oversize_reads_fail_cleanly() {
|
|
let (_base, r) = fixture("reads");
|
|
assert!(matches!(r.read("nope.txt", 100), Err(ResourceError::Io(_))));
|
|
assert!(matches!(r.read("sub", 100), Err(ResourceError::InvalidPath(_))));
|
|
assert!(matches!(r.read("a.txt", 4), Err(ResourceError::Budget(_))));
|
|
assert_eq!(r.read("a.txt", 5).unwrap(), b"alpha");
|
|
}
|
|
|
|
#[test]
|
|
fn asset_ids_follow_the_grammar() {
|
|
for ok in ["abc", "ABC-123_x", "tex.v2", "a", &"x".repeat(128)] {
|
|
assert!(validate_asset_id(ok).is_ok(), "{ok}");
|
|
}
|
|
for bad in [
|
|
"", "../x", "..", ".hidden", "a/b", "a\\b", "a..b", "a b", "é", "a:b", "a\0b", "/abs", &"x".repeat(129),
|
|
] {
|
|
assert!(matches!(validate_asset_id(bad), Err(ResourceError::InvalidAssetId(_))), "{bad:?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn decode_budgets_refuse_before_allocating() {
|
|
let mut budget = DecodeBudget::new(1000);
|
|
assert_eq!(budget.charge_image(10, 10, 4, "img").unwrap(), 400);
|
|
assert_eq!(budget.remaining(), 600);
|
|
// Over the remaining budget: refused, nothing taken.
|
|
assert!(matches!(budget.charge_image(20, 10, 4, "img"), Err(ResourceError::Budget(_))));
|
|
assert_eq!(budget.used(), 400);
|
|
// Overflowing sizes are errors, never wrapped into small numbers.
|
|
assert!(DecodeBudget::image_bytes(1 << 33, 1 << 31, 4).is_err());
|
|
assert!(DecodeBudget::image_bytes(u64::MAX, 2, 1).is_err());
|
|
assert!(budget.charge_image(u64::MAX, u64::MAX, 16, "bomb").is_err());
|
|
budget.release(400);
|
|
assert_eq!(budget.used(), 0);
|
|
assert!(budget.charge(1000, "all").is_ok());
|
|
assert!(budget.charge(1, "one more").is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn absolute_paths_are_read_only_inside_trusted_roots() {
|
|
let (base, r) = fixture("trusted");
|
|
let roots = [r.clone()];
|
|
// Inside the root, spelled with `..` through the root itself: fine.
|
|
let spelled = base.join("doc/sub/../a.txt");
|
|
assert_eq!(ResourceResolver::read_within(&roots, &spelled, 100).unwrap(), b"alpha");
|
|
assert_eq!(ResourceResolver::read_within(&roots, &base.join("doc/sub/b.txt"), 100).unwrap(), b"beta");
|
|
// Outside every root, or relative: refused.
|
|
let err = ResourceResolver::read_within(&roots, &base.join("secret.txt"), 100).unwrap_err();
|
|
assert!(matches!(err, ResourceError::Escape(_)), "{err}");
|
|
assert!(ResourceResolver::read_within(&roots, &base.join("doc/../secret.txt"), 100).is_err());
|
|
assert!(ResourceResolver::read_within(&roots, std::path::Path::new("a.txt"), 100).is_err());
|
|
assert!(ResourceResolver::read_within(&[], &base.join("doc/a.txt"), 100).is_err());
|
|
#[cfg(unix)]
|
|
{
|
|
// A link inside the root pointing out resolves outside: refused.
|
|
std::os::unix::fs::symlink(base.join("secret.txt"), r.root().join("out.txt")).unwrap();
|
|
assert!(ResourceResolver::read_within(&roots, &r.root().join("out.txt"), 100).is_err());
|
|
}
|
|
}
|