From 09bb101de18490b14cf4ef1b05615055cc68a47f Mon Sep 17 00:00:00 2001 From: Acts1631 Date: Thu, 23 Jul 2026 14:31:14 -0400 Subject: [PATCH] Fix PKG extraction path traversal PKG entry names could contain traversal components and escape the installation directory. Reject absolute and special components, then require the resolved target to remain below the installation root before creating or writing files. --- rpcs3/Crypto/unpkg.cpp | 68 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/rpcs3/Crypto/unpkg.cpp b/rpcs3/Crypto/unpkg.cpp index c9a49348a5..bffa5ea500 100644 --- a/rpcs3/Crypto/unpkg.cpp +++ b/rpcs3/Crypto/unpkg.cpp @@ -877,6 +877,25 @@ bool package_reader::fill_data(std::map& all_instal return false; } + std::error_code path_ec; + auto install_path = std::filesystem::weakly_canonical(m_install_path, path_ec); + if (path_ec) + { + pkg_log.warning("Failed to canonicalize installation path '%s' (%s); falling back to lexical normalization.", m_install_path, path_ec.message()); + install_path = std::filesystem::path(m_install_path).lexically_normal(); + } + + if (install_path.empty()) + { + pkg_log.error("Failed to normalize installation path for '%s'", m_install_path); + return false; + } + + const auto is_inside_install_path = [&install_path](const std::filesystem::path& path) + { + return std::mismatch(install_path.begin(), install_path.end(), path.begin(), path.end()).first == install_path.end(); + }; + m_install_entries.clear(); m_bootable_file_path.clear(); m_entry_indexer = 0; @@ -914,7 +933,46 @@ bool package_reader::fill_data(std::map& all_instal std::string_view name = fmt::trim_back_sv(name_buf, "\0"sv); + const std::filesystem::path entry_path{name}; + if (entry_path.is_absolute()) + { + num_failures++; + pkg_log.error("PKG entry path is absolute: '%s'", name); + break; + } + + for (const auto& component : entry_path) + { + if (component == "." || component == "..") + { + num_failures++; + pkg_log.error("PKG entry path contains a special component: '%s'", name); + break; + } + } + + if (num_failures) + { + break; + } + std::string path = m_install_path + vfs::escape(name); + path_ec.clear(); + auto canonical_path = std::filesystem::weakly_canonical(path, path_ec); + if (path_ec) + { + pkg_log.warning("Failed to canonicalize package path '%s' (%s); falling back to lexical normalization.", path, path_ec.message()); + canonical_path = std::filesystem::path(path).lexically_normal(); + } + + if (canonical_path.empty() || !is_inside_install_path(canonical_path)) + { + num_failures++; + pkg_log.error("PKG entry path escapes installation directory: '%s'", name); + break; + } + + path = canonical_path.string(); if (entry.pad || (entry.type & ~PKG_FILE_ENTRY_KNOWN_BITS)) { @@ -952,15 +1010,7 @@ bool package_reader::fill_data(std::map& all_instal default: { // TODO: check for valid utf8 characters - const std::string true_path = std::filesystem::path(path).lexically_normal().string(); - if (true_path.empty()) - { - num_failures++; - pkg_log.error("Failed to normalize package path for '%s'", path); - break; - } - - auto map_ptr = &*all_install_entries.try_emplace(true_path).first; + auto map_ptr = &*all_install_entries.try_emplace(path).first; m_install_entries.push_back({ .weak_reference = map_ptr,