#pragma once // Turns an untrusted (saveDir, filename) into a verified filesystem destination, or a // -32011. This is the process's one path-traversal boundary: the string can come from a // web page via capture.offer, or from any same-UID process via download.add. // // The rules and every adversarial case are in daemon/docs/safepath-adversarial.md, which // was written before this header. In short: sanitize the leaf in isolation; require an // absolute saveDir with no ".." component; walk it component-by-component with // openat(O_NOFOLLOW) (never stat-then-open), creating missing tail dirs with mkdirat; // then re-derive the final directory's canonical path from its fd and assert it is inside // a canonical allowed root, component-wise. #include #include #include #include #include namespace velox::daemon::fs { struct SafePathError { enum class Kind { not_absolute, // saveDir is relative or empty dotdot, // saveDir contains a ".." component outside_roots, // resolves outside every allowed root symlink_component, // a path component (or the leaf) is a symlink not_a_dir, // a component exists and is not a directory bad_leaf, // filename empty / "." / ".." / contains '/' / all control bytes name_too_long, // a component exceeds the filesystem limit io, // any other errno from the walk }; Kind kind = Kind::io; std::string message; }; // A verified destination. `dir` is absolute, canonical (symlink-free), exists as a // directory, and is inside an allowed root. `leaf` is a sanitized single component. struct SafeTarget { std::string dir; std::string leaf; std::string full() const { return dir + "/" + leaf; } }; // Sanitize one filename component: drop bytes < 0x20 and 0x7F, trim surrounding // whitespace, strip trailing dots and spaces, reject ""/"."/".."/contains-'/', and cap at // 255 bytes of UTF-8 without splitting a codepoint. Returns nullopt on reject. std::optional sanitize_leaf(std::string_view name); // Expand a leading "~" (to $HOME) and realpath() a configured root. Call once per entry in // saveTo.allowedRoots at startup / on settings.set; the result is what resolve_target // compares against. nullopt if the path does not currently resolve. std::optional canonicalize_root(std::string_view configured); // The gate. `save_dir` must be absolute and free of ".."; it is created (like `mkdir -p`) // if missing, but only ever inside a canonical root and only via an O_NOFOLLOW walk. // `filename_leaf` is sanitized here. `canonical_roots` is canonicalize_root() applied to // every allowed root (empty => nothing is permitted). std::expected resolve_target( std::string_view save_dir, std::string_view filename_leaf, const std::vector& canonical_roots); } // namespace velox::daemon::fs