diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp index 59f27b6ba6..c85d33eed2 100644 --- a/src/libslic3r/Utils.hpp +++ b/src/libslic3r/Utils.hpp @@ -266,8 +266,9 @@ extern bool is_symlink_target_within_root(const std::string &link_rel_path, cons // True if path names an entry strictly inside root: it must be spelled with root as its prefix, // and must still resolve inside root once symlinks are followed. extern bool is_absolute_path_within_root(const boost::filesystem::path &path, const boost::filesystem::path &root); -// True if opening a file with this name through the desktop would run it as a program or script. -extern bool is_executable_file_name(const std::string &file_name); +// True if a file with this name is of a type that the desktop opens as plain content, so it cannot run code. +// Anything unknown is not safe. +extern bool is_safe_to_open_file_name(const std::string &file_name); // Orca: custom protocal support utils inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); } diff --git a/src/libslic3r/utils.cpp b/src/libslic3r/utils.cpp index f4211347d4..f56ce6a7af 100644 --- a/src/libslic3r/utils.cpp +++ b/src/libslic3r/utils.cpp @@ -1134,26 +1134,20 @@ bool is_absolute_path_within_root(const boost::filesystem::path &path, const boo return !rel.empty() && rel != "." && is_path_within_root(rel.string(), root); } -bool is_executable_file_name(const std::string &file_name) +bool is_safe_to_open_file_name(const std::string &file_name) { - static const std::vector executable_extensions = { - // Windows - "exe", "com", "bat", "cmd", "scr", "pif", "msi", "msp", "msc", "cpl", "lnk", "url", "hta", "js", "jse", "vbs", - "vbe", "wsf", "wsh", "ps1", "psm1", "reg", "jar", "appref-ms", "scf", "inf", "py", "pyw", - // macOS - "app", "command", "pkg", "terminal", "workflow", - // Linux - "sh", "bash", "run", "desktop", "appimage"}; - // Windows ignores trailing dots and spaces, so "setup.exe." still runs as an .exe. - const size_t end = file_name.find_last_not_of(". "); - if (end == std::string::npos) + // Formats that cannot carry macros or scripts. Legacy and OpenDocument office files, HTML and SVG are left out on purpose. + static const std::vector safe_extensions = { + "jpg", "jpeg", "jfif", "pjpeg", "pjp", "png", "gif", "bmp", "webp", "tif", "tiff", + "pdf", "txt", "md", "csv", "docx", "xlsx", "pptx", + "stl", "obj", "3mf", "amf", "ply", "step", "stp", "iges", "igs", "dxf", + "mp4", "mov", "webm"}; + // The name must end in the extension itself: Windows drops trailing dots and spaces and reads ':' as a stream separator. + const size_t dot = file_name.find_last_of('.'); + if (dot == std::string::npos || file_name.find_first_of("/\\:") != std::string::npos) return false; - const std::string name = file_name.substr(0, end + 1); - const size_t dot = name.find_last_of('.'); - if (dot == std::string::npos || name.find_first_of("/\\", dot) != std::string::npos) - return false; - const std::string extension = boost::algorithm::to_lower_copy(name.substr(dot + 1)); - return std::find(executable_extensions.begin(), executable_extensions.end(), extension) != executable_extensions.end(); + const std::string extension = boost::algorithm::to_lower_copy(file_name.substr(dot + 1)); + return std::find(safe_extensions.begin(), safe_extensions.end(), extension) != safe_extensions.end(); } bool is_img_file(const std::string &path) diff --git a/src/slic3r/GUI/Auxiliary.cpp b/src/slic3r/GUI/Auxiliary.cpp index 444daa4f91..67998b59d9 100644 --- a/src/slic3r/GUI/Auxiliary.cpp +++ b/src/slic3r/GUI/Auxiliary.cpp @@ -428,7 +428,7 @@ void AuFile::on_dclick(wxMouseEvent &evt) if (m_type == AddFileButton) return; else - wxLaunchDefaultApplication(m_file_path.wstring(), 0); + desktop_open_project_attachment(this, m_file_path); } void AuFile::on_mouse_left_up(wxMouseEvent &evt) diff --git a/src/slic3r/GUI/GUI.cpp b/src/slic3r/GUI/GUI.cpp index 5301b91d86..d7bd96f178 100644 --- a/src/slic3r/GUI/GUI.cpp +++ b/src/slic3r/GUI/GUI.cpp @@ -30,11 +30,13 @@ #include "AboutDialog.hpp" #include "MsgDialog.hpp" +#include "Plater.hpp" #include "format.hpp" #include "WebUserLoginDialog.hpp" #include "libslic3r/Print.hpp" +#include "libslic3r/Utils.hpp" namespace Slic3r { @@ -643,4 +645,26 @@ void desktop_open_any_folder( const std::string& path ) } +bool desktop_open_project_attachment(wxWindow *parent, const boost::filesystem::path &path) +{ + // The auxiliary path is UTF-8, which is what boost::filesystem reads a narrow string as. + const boost::filesystem::path aux_root(wxGetApp().plater()->model().get_auxiliary_file_temp_path()); + boost::system::error_code ec; + if (!is_absolute_path_within_root(path, aux_root) || !boost::filesystem::is_regular_file(path, ec)) + return false; + + // Attachments come with the project and carry no download mark, so the desktop would open them without a warning. + if (!is_safe_to_open_file_name(path.filename().string())) { + MessageDialog dlg(parent, + wxString::Format(_L("\"%s\" is not a plain document, image or model file. Opening it may run it as a " + "program or script on this computer.\n\n" + "Only open attachments from projects you trust. Open it anyway?"), + from_path(path.filename())), + _L("Open attachment"), wxICON_WARNING | wxYES_NO); + if (dlg.ShowModal() != wxID_YES) + return false; + } + return wxLaunchDefaultApplication(from_path(path), 0); +} + } } diff --git a/src/slic3r/GUI/GUI.hpp b/src/slic3r/GUI/GUI.hpp index 0b3ac6d7b5..a5bbb35f6f 100644 --- a/src/slic3r/GUI/GUI.hpp +++ b/src/slic3r/GUI/GUI.hpp @@ -86,6 +86,9 @@ extern void about(); extern void desktop_open_datadir_folder(); // Ask the destop to open one folder extern void desktop_open_any_folder(const std::string& path); +// Ask the desktop to open a file from the project's auxiliary directory, after a confirmation +// unless its type is known to be plain content. Returns false if the file was not opened. +extern bool desktop_open_project_attachment(wxWindow *parent, const boost::filesystem::path &path); } // namespace GUI } // namespace Slic3r diff --git a/src/slic3r/GUI/Project.cpp b/src/slic3r/GUI/Project.cpp index 8d45c94e7f..127400c8b1 100644 --- a/src/slic3r/GUI/Project.cpp +++ b/src/slic3r/GUI/Project.cpp @@ -27,7 +27,6 @@ #include "GUI_App.hpp" #include "GUI_ObjectList.hpp" #include "MainFrame.hpp" -#include "MsgDialog.hpp" #include namespace Slic3r { namespace GUI { @@ -294,25 +293,8 @@ void ProjectPanel::OnScriptMessage(wxWebViewEvent& evt) if (!accessory_path.empty()) { std::string decode_path = wxGetApp().url_decode(accessory_path.ToStdString()); fs::path path(decode_path); - // Only open the project's own extracted auxiliary files, with the root built as in Reload(). - fs::path aux_root(encode_path(wxGetApp().plater()->model().get_auxiliary_file_temp_path().c_str())); - - if (is_absolute_path_within_root(path, aux_root) && fs::is_regular_file(path)) { - // Attachments come with the project, so ask before running one that is a program or script. - bool open = true; - if (is_executable_file_name(path.filename().string())) { - MessageDialog dlg(this, - wxString::Format(_L("\"%s\" is a program or script. Opening it will run it on this computer.\n\n" - "Only open attachments from projects you trust. Open it anyway?"), - from_path(path.filename())), - _L("Open attachment"), wxICON_WARNING | wxYES_NO); - open = dlg.ShowModal() == wxID_YES; - } - if (open) - wxLaunchDefaultApplication(path.wstring(), 0); - } else { - BOOST_LOG_TRIVIAL(warning) << "open_3mf_accessory: ignoring path outside the project auxiliary directory: " << decode_path; - } + if (!desktop_open_project_attachment(this, path)) + BOOST_LOG_TRIVIAL(warning) << "open_3mf_accessory: not opening " << decode_path; } } else if (strCmd == "request_3mf_info") { diff --git a/tests/libslic3r/test_utils.cpp b/tests/libslic3r/test_utils.cpp index 7ff0f419fa..6256e5f044 100644 --- a/tests/libslic3r/test_utils.cpp +++ b/tests/libslic3r/test_utils.cpp @@ -368,19 +368,22 @@ TEST_CASE("is_absolute_path_within_root accepts only entries inside the root", " #endif } -TEST_CASE("is_executable_file_name flags attachments that would run as programs", "[utils]") { - const std::string executable = GENERATE(as{}, - "setup.exe", "SETUP.EXE", "Manual.pdf.exe", "run.bat", "start.cmd", "shortcut.lnk", "site.url", "script.ps1", - "macro.vbs", "tool.jar", "script.py", "Install.command", "Tool.app", "installer.pkg", "install.sh", - "launcher.desktop", "Printer.AppImage", "setup.exe.", "setup.exe ", "setup.exe. .", ".exe"); - INFO(executable); - CHECK(is_executable_file_name(executable)); +TEST_CASE("is_safe_to_open_file_name accepts plain documents, images and models", "[utils]") { + const std::string safe = GENERATE(as{}, + "Manual.pdf", "BOM.xlsx", "BOM.csv", "guide.docx", "notes.txt", "README.md", "photo.JPG", "render.png", + "assembly.step", "part.stl", "project.3mf", "drawing.dxf", "build.mp4", "setup.exe.pdf", ".pdf"); + INFO(safe); + CHECK(is_safe_to_open_file_name(safe)); } -TEST_CASE("is_executable_file_name leaves documents and models alone", "[utils]") { - const std::string document = GENERATE(as{}, - "Manual.pdf", "BOM.xlsx", "notes.txt", "photo.JPG", "assembly.step", "part.stl", "project.3mf", "readme", - "exe", "setup.exe.pdf", "", "...", "dir.exe/readme"); - INFO(document); - CHECK_FALSE(is_executable_file_name(document)); +TEST_CASE("is_safe_to_open_file_name rejects programs and anything it does not know", "[utils]") { + const std::string unsafe = GENERATE(as{}, + "setup.exe", "SETUP.EXE", "Manual.pdf.exe", "run.bat", "shortcut.lnk", "site.url", "script.ps1", "help.chm", + "tool.jar", "script.py", "Install.command", "install.sh", "launcher.desktop", "Printer.AppImage", + // Documents that can carry macros or scripts. + "BOM.xls", "BOM.xlsm", "guide.doc", "guide.docm", "sheet.ods", "page.html", "logo.svg", "bundle.zip", + // No extension, an unknown one, or a name the desktop would read differently. + "readme", "pdf", "data.xyz", "", "...", "Manual.pdf.", "Manual.pdf ", "setup.exe:note.txt", "dir.pdf/readme"); + INFO(unsafe); + CHECK_FALSE(is_safe_to_open_file_name(unsafe)); }