Validate Plugin Symlink Targets Before Replacing Existing Files

A symlink entry's target is now read and checked before anything already
at its destination is removed or renamed aside, so an archive rejected
for its link target leaves the installed plugin files in place.
This commit is contained in:
Hanif Koh
2026-09-29 22:37:43 +08:00
parent 78b504be0e
commit 2ae1fdbd01
+17 -9
View File
@@ -1520,6 +1520,21 @@ int GUI_App::install_plugin(std::string name, std::string package_name, InstallP
} }
auto dest_path = plugin_folder / dest_file; auto dest_path = plugin_folder / dest_file;
std::string dest_zip_file = encode_path(dest_path.string().c_str()); std::string dest_zip_file = encode_path(dest_path.string().c_str());
#ifndef WIN32
// Validate a symlink's target before anything at the destination is replaced.
const bool is_link = S_ISLNK(stat.m_external_attr >> 16);
std::string link;
if (is_link) {
link.assign(stat.m_uncomp_size, 0);
if (!mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, link.data(), stat.m_uncomp_size, 0) ||
!is_symlink_target_within_root(dest_file, link, plugin_folder)) {
BOOST_LOG_TRIVIAL(error) << "[install_plugin] link " << dest_file << " -> " << link << " is unreadable or resolves outside " << plugin_folder.string();
close_zip_reader(&archive);
if (pro_fn) { pro_fn(InstallStatusUnzipFailed, 0, cancel); }
return InstallStatusUnzipFailed;
}
}
#endif
try { try {
boost::filesystem::create_directories(dest_path.parent_path()); boost::filesystem::create_directories(dest_path.parent_path());
// symlink_status so that an existing symlink, dangling or not, is replaced rather than written through. // symlink_status so that an existing symlink, dangling or not, is replaced rather than written through.
@@ -1551,15 +1566,8 @@ int GUI_App::install_plugin(std::string name, std::string package_name, InstallP
} }
mz_bool res = 0; mz_bool res = 0;
#ifndef WIN32 #ifndef WIN32
if (S_ISLNK(stat.m_external_attr >> 16)) { if (is_link) {
std::string link(stat.m_uncomp_size, 0); res = 1;
res = mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, link.data(), stat.m_uncomp_size, 0);
if (res && !is_symlink_target_within_root(dest_file, link, plugin_folder)) {
BOOST_LOG_TRIVIAL(error) << "[install_plugin] link " << dest_file << " -> " << link << " resolves outside " << plugin_folder.string();
close_zip_reader(&archive);
if (pro_fn) { pro_fn(InstallStatusUnzipFailed, 0, cancel); }
return InstallStatusUnzipFailed;
}
try { try {
boost::filesystem::create_symlink(link, dest_path); boost::filesystem::create_symlink(link, dest_path);
} catch (const std::exception &e) { } catch (const std::exception &e) {