Open Project Attachments Through One Guarded Helper

The Edit Project Info view launched attachments directly, without the
checks the project page has. Both now call
desktop_open_project_attachment, which checks that the file is inside
the auxiliary directory, asks for confirmation where needed and then
opens it.

The auxiliary root was built through encode_path, which returns code
page bytes on Windows, while boost::filesystem reads a narrow string as
UTF-8. With a non-ASCII temporary directory the root never matched and
no attachment opened. It is now built from the UTF-8 path directly.

The list of program extensions could not be kept complete and let
unknown types open without a prompt. It is replaced by
is_safe_to_open_file_name, a list of plain document, image, model and
video types that open directly. Everything else asks first.
This commit is contained in:
Hanif Koh
2026-09-29 23:50:10 +08:00
parent 40028f7b61
commit f2c856a37e
7 changed files with 61 additions and 54 deletions
+3 -2
View File
@@ -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"); }
+12 -18
View File
@@ -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<std::string> 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<std::string> 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)
+1 -1
View File
@@ -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)
+24
View File
@@ -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);
}
} }
+3
View File
@@ -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
+2 -20
View File
@@ -27,7 +27,6 @@
#include "GUI_App.hpp"
#include "GUI_ObjectList.hpp"
#include "MainFrame.hpp"
#include "MsgDialog.hpp"
#include <slic3r/GUI/Widgets/WebView.hpp>
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") {
+16 -13
View File
@@ -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<std::string>{},
"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<std::string>{},
"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<std::string>{},
"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<std::string>{},
"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));
}