From 5b4c29b12393f9da26a55408ea179d4813ca2c45 Mon Sep 17 00:00:00 2001 From: Maufeat Date: Sat, 25 Jul 2026 00:31:22 +0200 Subject: [PATCH] [file_sys] Quick sandbox escape fix (#4223) Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4223 Reviewed-by: Lizzie Reviewed-by: MaranBr --- src/common/fs/path_util.cpp | 23 +++++++++++++++++++++++ src/core/file_sys/vfs/vfs_real.cpp | 18 +++++++++++++++--- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp index 3844cca06b..4105855866 100644 --- a/src/common/fs/path_util.cpp +++ b/src/common/fs/path_util.cpp @@ -476,6 +476,29 @@ std::string SanitizePath(std::string_view path_, DirectorySeparator directory_se path.erase(std::unique(start, path.end(), [type2](char c1, char c2) { return c1 == type2 && c2 == type2; }), path.end()); + + const bool absolute = !path.empty() && path[0] == type2; + std::vector parts; + + for (const auto part : SplitPathComponents(path)) + { + if (part.empty() || part == ".") + continue; + if (part == ".." && !parts.empty() && parts.back() != "..") + parts.pop_back(); + else if (part != "..") parts.push_back(part); + } + + std::string resolved = absolute ? std::string(1, type2) : std::string{}; + for (std::size_t i = 0; i < parts.size(); ++i) + { + if (i != 0) + resolved += type2; + resolved.append(parts[i].data(), parts[i].size()); + } + + path = std::move(resolved); + return std::string(RemoveTrailingSlash(path)); } diff --git a/src/core/file_sys/vfs/vfs_real.cpp b/src/core/file_sys/vfs/vfs_real.cpp index 05774b67d5..b04f832cd3 100644 --- a/src/core/file_sys/vfs/vfs_real.cpp +++ b/src/core/file_sys/vfs/vfs_real.cpp @@ -35,6 +35,16 @@ namespace { constexpr size_t MaxOpenFiles = 8192; +bool IsWithinRoot(std::string_view root, std::string_view full_path) { + if (root.empty()) + return true; + + if (full_path.size() < root.size() || full_path.substr(0, root.size()) != root) + return false; + + return full_path.size() == root.size() || full_path[root.size()] == '/' || full_path[root.size()] == '\\'; +} + constexpr FS::FileAccessMode ModeFlagsToFileAccessMode(OpenMode mode) { switch (mode) { case OpenMode::Read: @@ -403,7 +413,8 @@ RealVfsDirectory::~RealVfsDirectory() = default; VirtualFile RealVfsDirectory::GetFileRelative(std::string_view relative_path) const { const auto full_path = FS::SanitizePath(path + '/' + std::string(relative_path)); - if (!FS::Exists(full_path) || FS::IsDir(full_path)) { + if (!FS::Exists(full_path) || FS::IsDir(full_path) + || !IsWithinRoot(FS::SanitizePath(path), full_path)) { return nullptr; } return base.OpenFile(full_path, perms); @@ -411,7 +422,8 @@ VirtualFile RealVfsDirectory::GetFileRelative(std::string_view relative_path) co VirtualDir RealVfsDirectory::GetDirectoryRelative(std::string_view relative_path) const { const auto full_path = FS::SanitizePath(path + '/' + std::string(relative_path)); - if (!FS::Exists(full_path) || !FS::IsDir(full_path)) { + if (!FS::Exists(full_path) || !FS::IsDir(full_path) + || !IsWithinRoot(FS::SanitizePath(path), full_path)) { return nullptr; } return base.OpenDirectory(full_path, perms); @@ -427,7 +439,7 @@ VirtualDir RealVfsDirectory::GetSubdirectory(std::string_view name) const { VirtualFile RealVfsDirectory::CreateFileRelative(std::string_view relative_path) { const auto full_path = FS::SanitizePath(path + '/' + std::string(relative_path)); - if (!FS::CreateParentDirs(full_path)) { + if (!FS::CreateParentDirs(full_path) || !IsWithinRoot(FS::SanitizePath(path), full_path)) { return nullptr; } return base.CreateFile(full_path, perms);