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+='
'+ - '
'+ - '
'+ - '
'+tName+'
'+ - '
'+ - '
'+ - '
'; + pMenu.on('click', function(){ OnClickOpenFile(tPath); }); } else { ImgID++; let TmpImgID="AF"+ImgID; - strHtml+='
'+ - '
'+ - '
'+ - '
'+tName+'
'+ - '
'+ - '
'+ - '
'; + 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 +}