diff --git a/resources/web/model/model.js b/resources/web/model/model.js
index b401bea75b..a6918d15d2 100644
--- a/resources/web/model/model.js
+++ b/resources/web/model/model.js
@@ -214,9 +214,9 @@ function ShowModelInfo( pModel )
SendWXDebugInfo("Model Name: "+sModelName);
- $('#ModelName').html(sModelName);
+ $('#ModelName').text(sModelName);
$('#ModelName').attr('title',sModelName);
- $('#ModelAuthorName').html(sModelAuthor);
+ $('#ModelAuthorName').text(sModelAuthor);
switch(UploadType)
{
@@ -268,7 +268,7 @@ function ShowModelInfo( pModel )
break;
}
- $('#Model_Desc').html( html_decode(sModelDesc) );
+ $('#Model_Desc').empty().append( SanitizeDescHtml( html_decode(sModelDesc) ) );
let ModelPreviewList=pModel.preview_img;
let TotalPreview=ModelPreviewList.length;
@@ -410,7 +410,8 @@ function ConstructFileHtml( ID, pItem )
{
let fTotal=pItem.length;
- let strHtml='';
+ let pBoard=$('#'+ID+' .FileListBoard');
+ pBoard.empty();
for( let f=0;f').attr('src',ImgPath);
+ let pMenu=$('
');
if( strClass!='ImageIcon' )
{
- strHtml+=''+
- '
'+
- '
'+
- ' '+
- '
';
+ pMenu.on('click', function(){ OnClickOpenFile(tPath); });
}
else
{
ImgID++;
let TmpImgID="AF"+ImgID;
- strHtml+=''+
- '
'+
- '
'+
- ' '+
- '
';
+ pIconImg.attr('id',TmpImgID);
+ pMenu.on('click', function(){ OnClickOpenImage(TmpImgID); });
}
+
+ let pFileItem=$('');
+ pFileItem.append( $('').addClass(strClass).append(pIconImg) );
+ pFileItem.append( $('').append( $('').text(tName) ) );
+ pFileItem.append( pMenu );
+ pBoard.append( pFileItem );
}
- $('#'+ID+' .FileListBoard').html(strHtml);
-
if( fTotal>0 )
$('#'+ID).show();
}
+// Descriptions are untrusted 3MF metadata that may carry rich-text HTML (e.g. from MakerWorld).
+// Rebuild them from an inert parse, keeping only plain formatting tags and http(s) links and images.
+var DescAllowedTags=['P','BR','B','STRONG','I','EM','U','S','STRIKE','SUB','SUP','SMALL','MARK',
+ 'H1','H2','H3','H4','H5','H6','UL','OL','LI','BLOCKQUOTE','PRE','CODE','HR','SPAN','DIV',
+ 'TABLE','THEAD','TBODY','TFOOT','TR','TH','TD','A','IMG'];
+// Dropped together with their content; any other unknown tag is unwrapped to its children.
+var DescDroppedTags=['SCRIPT','STYLE','TEMPLATE','NOSCRIPT','TEXTAREA','TITLE','IFRAME','FRAME','OBJECT','EMBED','SVG','MATH'];
+
+function IsHttpUrl( strUrl )
+{
+ return /^\s*https?:\/\//i.test(strUrl||'');
+}
+
+function CopyDescNodes( pSrc, pDst )
+{
+ for( let pNode=pSrc.firstChild;pNode!=null;pNode=pNode.nextSibling )
+ {
+ if( pNode.nodeType==Node.TEXT_NODE )
+ {
+ pDst.appendChild( document.createTextNode(pNode.nodeValue) );
+ continue;
+ }
+ if( pNode.nodeType!=Node.ELEMENT_NODE )
+ continue;
+
+ let sTag=pNode.nodeName.toUpperCase();
+ if( $.inArray(sTag,DescDroppedTags)>=0 )
+ continue;
+ if( $.inArray(sTag,DescAllowedTags)<0 )
+ {
+ CopyDescNodes(pNode,pDst);
+ continue;
+ }
+
+ let pElem=document.createElement(sTag);
+ if( sTag=='A' && IsHttpUrl(pNode.getAttribute('href')) )
+ pElem.setAttribute('href',pNode.getAttribute('href'));
+ else if( sTag=='IMG' )
+ {
+ if( !IsHttpUrl(pNode.getAttribute('src')) )
+ continue;
+ pElem.setAttribute('src',pNode.getAttribute('src'));
+ }
+ CopyDescNodes(pNode,pElem);
+ pDst.appendChild(pElem);
+ }
+}
+
+function SanitizeDescHtml( strHtml )
+{
+ let pFragment=document.createDocumentFragment();
+ // A DOMParser document is inert: it runs no scripts and loads no resources.
+ let pDoc=new DOMParser().parseFromString(strHtml,'text/html');
+ if( pDoc && pDoc.body )
+ CopyDescNodes(pDoc.body,pFragment);
+ return pFragment;
+}
+
function ShowProfilelInfo( pProfile )
{
//==========Profile Info==========
@@ -483,10 +537,10 @@ function ShowProfilelInfo( pProfile )
let sProfileAuthor=decodeURIComponent(pProfile.author);
let sProfileDesc=decodeURIComponent(pProfile.description);
- $('#ProfileName').html(sProfileName);
- $('#ProfileAuthor').html(sProfileAuthor);
+ $('#ProfileName').text(sProfileName);
+ $('#ProfileAuthor').text(sProfileAuthor);
- $('#Profile_Desc').html( html_decode(sProfileDesc) );
+ $('#Profile_Desc').empty().append( SanitizeDescHtml( html_decode(sProfileDesc) ) );
let ProfilePreviewList=pProfile.preview_img;
let TotalPreview=ProfilePreviewList.length;
diff --git a/src/libslic3r/Utils.hpp b/src/libslic3r/Utils.hpp
index eff7f51c2a..d4062e07b1 100644
--- a/src/libslic3r/Utils.hpp
+++ b/src/libslic3r/Utils.hpp
@@ -263,6 +263,9 @@ extern bool is_path_within_root(const std::string &rel_path, const boost::filesy
// True if a symlink stored at link_rel_path (relative to root) with this target stays inside root: the target
// must be relative, and joined to the link's directory it must pass is_path_within_root.
extern bool is_symlink_target_within_root(const std::string &link_rel_path, const std::string &target, const boost::filesystem::path &root);
+// 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);
// 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 303476f023..5e795438f1 100644
--- a/src/libslic3r/utils.cpp
+++ b/src/libslic3r/utils.cpp
@@ -1127,6 +1127,12 @@ bool is_symlink_target_within_root(const std::string &link_rel_path, const std::
return is_path_within_root((sep == std::string::npos ? std::string() : link_rel_path.substr(0, sep + 1)) + target, root);
}
+bool is_absolute_path_within_root(const boost::filesystem::path &path, const boost::filesystem::path &root)
+{
+ const boost::filesystem::path rel = path.lexically_relative(root);
+ return !rel.empty() && rel != "." && is_path_within_root(rel.string(), root);
+}
+
bool is_img_file(const std::string &path)
{
return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg");
diff --git a/src/slic3r/GUI/Project.cpp b/src/slic3r/GUI/Project.cpp
index 7833cc7499..1ccb269f51 100644
--- a/src/slic3r/GUI/Project.cpp
+++ b/src/slic3r/GUI/Project.cpp
@@ -293,9 +293,13 @@ 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 (fs::exists(path)) {
+ if (is_absolute_path_within_root(path, aux_root) && fs::is_regular_file(path)) {
wxLaunchDefaultApplication(path.wstring(), 0);
+ } else {
+ BOOST_LOG_TRIVIAL(warning) << "open_3mf_accessory: ignoring path outside the project auxiliary directory: " << decode_path;
}
}
}
diff --git a/tests/libslic3r/test_utils.cpp b/tests/libslic3r/test_utils.cpp
index e2b220d482..ca5d9e07fc 100644
--- a/tests/libslic3r/test_utils.cpp
+++ b/tests/libslic3r/test_utils.cpp
@@ -322,3 +322,48 @@ TEST_CASE("is_symlink_target_within_root rejects a target that passes through a
CHECK(is_symlink_target_within_root("link", "in/lib.so", root));
}
#endif
+
+TEST_CASE("is_absolute_path_within_root accepts only entries inside the root", "[utils]") {
+ namespace fs = boost::filesystem;
+ ScopedTemporaryDir outer;
+ const fs::path root = outer.path() / "Auxiliaries";
+ fs::create_directories(root / "Others");
+ const fs::path inside = root / "Others" / "note.txt";
+ const fs::path outside = outer.path() / "secret.txt";
+ std::ofstream(inside.string()) << "inside";
+ std::ofstream(outside.string()) << "outside";
+
+ SECTION("a file inside the root") {
+ REQUIRE(is_absolute_path_within_root(inside, root));
+ }
+ SECTION("a path inside the root whose file does not exist yet") {
+ REQUIRE(is_absolute_path_within_root(root / "Others" / "missing.txt", root));
+ }
+ SECTION("the root itself") {
+ REQUIRE_FALSE(is_absolute_path_within_root(root, root));
+ }
+ SECTION("a parent-directory escape spelled under the root") {
+ REQUIRE_FALSE(is_absolute_path_within_root(root / "Others" / ".." / ".." / "secret.txt", root));
+ }
+ SECTION("an absolute path elsewhere") {
+ REQUIRE_FALSE(is_absolute_path_within_root(outside, root));
+ }
+ SECTION("a sibling directory sharing the root's name as a prefix") {
+ const fs::path sibling = outer.path() / "Auxiliaries2" / "note.txt";
+ REQUIRE_FALSE(is_absolute_path_within_root(sibling, root));
+ }
+ SECTION("a relative path") {
+ REQUIRE_FALSE(is_absolute_path_within_root(fs::path("Others") / "note.txt", root));
+ }
+ SECTION("an empty path") {
+ REQUIRE_FALSE(is_absolute_path_within_root(fs::path(), root));
+ }
+#ifndef _WIN32
+ // Creating symlinks on Windows needs elevated rights or developer mode.
+ SECTION("a symlink inside the root that points outside") {
+ const fs::path link = root / "Others" / "link.txt";
+ fs::create_symlink(outside, link);
+ REQUIRE_FALSE(is_absolute_path_within_root(link, root));
+ }
+#endif
+}