diff --git a/rpcs3/Crypto/unpkg.cpp b/rpcs3/Crypto/unpkg.cpp index c9a49348a53d..ae3097362077 100644 --- a/rpcs3/Crypto/unpkg.cpp +++ b/rpcs3/Crypto/unpkg.cpp @@ -121,9 +121,9 @@ bool package_reader::read_header() return false; } - if (u64{umax} / sizeof(PKGEntry) < u64(m_header.file_count)) + if (m_header.file_count > PKG_MAX_FILE_COUNT || u64(m_header.file_count) > u64(m_header.data_size) / sizeof(PKGEntry)) { - pkg_log.error("PKG file count is too large! (0x%x)", m_header.file_count); + pkg_log.error("PKG file count is invalid! (count=0x%x, data_size=0x%llx)", m_header.file_count, m_header.data_size); return false; } @@ -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,51 @@ 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 == "..") + { + fmt::throw_exception("PKG entry path contains a parent directory component: '%s'", name); + } + + if (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 +1015,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, diff --git a/rpcs3/Crypto/unpkg.h b/rpcs3/Crypto/unpkg.h index f7802492af7e..3579104b74e8 100644 --- a/rpcs3/Crypto/unpkg.h +++ b/rpcs3/Crypto/unpkg.h @@ -14,6 +14,7 @@ enum : u32 { PKG_HEADER_SIZE = 0xC0, // sizeof(pkg_header) + sizeof(pkg_unk_checksum) PKG_HEADER_SIZE2 = 0x280, + PKG_MAX_FILE_COUNT = 1'000'000, PKG_MAX_FILENAME_SIZE = 256, };