// The path-traversal boundary. Every row here is a case from // daemon/docs/safepath-adversarial.md. Uses a real temp tree as the allowed root. #include #include #include #include #include #include #include "check.hpp" #include "fs/safepath.hpp" using namespace velox::daemon::fs; using K = SafePathError::Kind; namespace { std::string g_root; // canonical allowed root (a temp dir) std::string g_outside; // a canonical dir outside the root std::vector roots() { return {g_root}; } bool is_err(const std::expected& r, K k) { return !r.has_value() && r.error().kind == k; } } // namespace void run() { char t1[] = "/tmp/velox-sp-root-XXXXXX"; char t2[] = "/tmp/velox-sp-out-XXXXXX"; g_root = ::mkdtemp(t1); g_outside = ::mkdtemp(t2); CHECK(!g_root.empty() && !g_outside.empty()); // realpath the root the way canonicalize_root would (⦅/tmp⦆ is a symlink on some distros). g_root = canonicalize_root(g_root).value_or(g_root); g_outside = canonicalize_root(g_outside).value_or(g_outside); // --- happy paths ------------------------------------------------------------- { auto r = resolve_target(g_root, "iso.img", roots()); CHECK(r.has_value()); if (r) { CHECK_EQ(r->dir, g_root); CHECK_EQ(r->leaf, std::string("iso.img")); } } { // A19: redundant "." and a fresh subdir created within the root auto r = resolve_target(g_root + "/./sub/.", "x", roots()); CHECK(r.has_value()); if (r) CHECK_EQ(r->dir, g_root + "/sub"); } { // A18: non-ASCII is fine auto r = resolve_target(g_root + "/新しい", "映画.mkv", roots()); CHECK(r.has_value()); } // --- traversal / containment ------------------------------------------------ CHECK(is_err(resolve_target(g_root + "/../etc", "x", roots()), K::dotdot)); // A1 CHECK(is_err(resolve_target(g_root + "/a/b/../../../etc", "x", roots()), K::dotdot)); // A2 CHECK(is_err(resolve_target("/etc", "x", roots()), K::outside_roots)); // A3 CHECK(is_err(resolve_target(g_root + "-evil", "x", roots()), K::outside_roots)); // A4 CHECK(is_err(resolve_target("relative/path", "x", roots()), K::not_absolute)); // A21 CHECK(is_err(resolve_target("", "x", roots()), K::not_absolute)); // A15/A21 // --- symlink component points outside (A7) -------------------------------- { const std::string link = g_root + "/link-out"; ::symlink(g_outside.c_str(), link.c_str()); auto r = resolve_target(link, "x", roots()); CHECK(is_err(r, K::symlink_component) || is_err(r, K::outside_roots)); // Even naming a path *through* the symlink must not escape. auto r2 = resolve_target(link + "/deeper", "x", roots()); CHECK(is_err(r2, K::symlink_component) || is_err(r2, K::outside_roots)); ::unlink(link.c_str()); } // --- destination is a file, not a dir (A17) ----------------------------- { const std::string f = g_root + "/a-file"; ::close(::open(f.c_str(), O_WRONLY | O_CREAT | O_EXCL, 0644)); CHECK(is_err(resolve_target(f, "x", roots()), K::not_a_dir)); ::unlink(f.c_str()); } // --- leaf sanitization ------------------------------------------------ CHECK(is_err(resolve_target(g_root, "../.bashrc", roots()), K::bad_leaf)); // A5 CHECK(is_err(resolve_target(g_root, "sub/dir/file", roots()), K::bad_leaf)); // A6 CHECK(is_err(resolve_target(g_root, std::string("f\0.iso", 6), roots()), K::bad_leaf) || resolve_target(g_root, std::string("f\0.iso", 6), roots()).value().leaf == "f.iso"); // A9: NUL stripped CHECK(is_err(resolve_target(g_root, ".", roots()), K::bad_leaf)); // A11 CHECK(is_err(resolve_target(g_root, "", roots()), K::bad_leaf)); // A11 // sanitize_leaf directly for the finicky cases CHECK(!sanitize_leaf(std::string("\r\nSet-Cookie: x").append(".iso")).has_value() || sanitize_leaf(std::string("\r\nSet-Cookie: x").append(".iso")).value().find('\n') == std::string::npos); // A10 CHECK_EQ(sanitize_leaf(" spaced.iso ").value(), std::string("spaced.iso")); // A20 CHECK_EQ(sanitize_leaf("dots...").value(), std::string("dots")); // A20 CHECK(!sanitize_leaf("...").has_value()); CHECK_EQ(sanitize_leaf("con").value(), std::string("con")); // A12: allowed on Linux { std::string huge(300, 'a'); auto s = sanitize_leaf(huge); CHECK(s.has_value()); if (s) CHECK(s->size() <= 255); // A13 } { // A13: a multibyte codepoint straddling the 255-byte cut is not split. std::string s(253, 'a'); s += "\xE2\x82\xAC"; // euro sign, 3 bytes -> ends at 256, must be dropped whole auto out = sanitize_leaf(s); CHECK(out.has_value()); if (out) { CHECK(out->size() <= 255); // no trailing partial sequence CHECK((static_cast(out->back()) & 0x80) == 0); } } // --- an empty root list permits nothing -------------------------------- CHECK(is_err(resolve_target(g_root, "x", {}), K::outside_roots)); // --- a symlinked root canonicalizes to its target (A16) -------------- { char t3[] = "/tmp/velox-sp-realroot-XXXXXX"; const std::string real_root = ::mkdtemp(t3); const std::string link_root = g_outside + "/root-link"; ::symlink(real_root.c_str(), link_root.c_str()); const auto canon = canonicalize_root(link_root); CHECK(canon.has_value()); if (canon) { CHECK_EQ(*canon, canonicalize_root(real_root).value()); auto r = resolve_target(link_root + "/movies", "a.mkv", {*canon}); CHECK(r.has_value()); } ::unlink(link_root.c_str()); ::rmdir(real_root.c_str()); } } TEST_MAIN()