From edf3a2865777e6a02be0e0f831358ddbed8bb0af Mon Sep 17 00:00:00 2001 From: offtkp Date: Tue, 4 Jun 2024 16:35:19 +0300 Subject: [PATCH] Fix path sanitization --- include/fs/archive_base.hpp | 40 ++++++++++++------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/include/fs/archive_base.hpp b/include/fs/archive_base.hpp index 2843be687..6a4dfe67b 100644 --- a/include/fs/archive_base.hpp +++ b/include/fs/archive_base.hpp @@ -172,48 +172,34 @@ class ArchiveBase { Memory& mem; // Returns if a specified 3DS path in UTF16 or ASCII format is safe or not - // A 3DS path is considered safe if its first character is '/' which means we're not trying to access anything outside the root of the fs - // And if it doesn't contain enough instances of ".." (Indicating "climb up a folder" in filesystems) to let the software climb up the directory tree - // And access files outside of the emulator's app data folder + // A 3DS path is considered safe if it has IOFile::getAppData() as its parent directory template bool isPathSafe(const FSPath& path) { static_assert(format == PathType::ASCII || format == PathType::UTF16); using String = typename std::conditional::type; // String type for the path using Char = typename String::value_type; // Char type for the path - String pathString, dots; + String pathString; if constexpr (std::is_same()) { pathString = path.utf16_string; - dots = u".."; } else { pathString = path.string; - dots = ".."; } - // If the path string doesn't begin with / then that means it's accessing outside the FS root, which is invalid & unsafe if (pathString[0] != Char('/')) return false; - // Counts how many folders sans the root our file is nested under. - // If it's < 0 at any point of parsing, then the path is unsafe and tries to crawl outside our file sandbox. - // If it's 0 then this is the FS root. - // If it's > 0 then we're in a subdirectory of the root. - int level = 0; - - // Split the string on / characters and see how many of the substrings are ".." - size_t pos = 0; - while ((pos = pathString.find(Char('/'))) != String::npos) { - String token = pathString.substr(0, pos); - pathString.erase(0, pos + 1); - - if (token == dots) { - level--; - if (level < 0) return false; - } else { - level++; - } - } + // Usually std::filesystem::canonical is used, as canonical also resolves symlinks, but requires that the path exists, + // which isn't always the case for paths that end up in this function. + // lexically_normal simply removes '.' and '..' from the path + // Since we can make the assumption that there's no symlinks in our sandbox, we can use lexically_normal + std::filesystem::path pathFs = IOFile::getAppData() / pathString.substr(1); // remove the leading slash, otherwise concatenation fails + pathFs = pathFs.lexically_normal(); + + std::filesystem::path sandboxPath = IOFile::getAppData().lexically_normal(); + + auto [it, _] = std::mismatch(sandboxPath.begin(), sandboxPath.end(), pathFs.begin()); - return true; + return it == sandboxPath.end(); } public: