mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-30 20:31:09 +00:00
Escape Project Metadata in the Project Page and Restrict Accessory Opening
The Project page rendered the model and profile name, author, description and accessory file names from the 3MF as live HTML. Names, authors and file names are now set as text, and the file list is built from DOM nodes with bound click handlers instead of concatenated markup. Descriptions can legitimately carry rich-text HTML, so they are rebuilt from an inert DOMParser document, keeping only plain formatting tags, http(s) links and http(s) images, with every other attribute dropped. Opening an accessory from the page now only launches regular files that lie inside the project's extracted auxiliary directory. The containment check is a new libslic3r helper, is_absolute_path_within_root, built on is_path_within_root so symlinks leading out of the root are rejected too.
This commit is contained in:
@@ -214,9 +214,9 @@ function ShowModelInfo( pModel )
|
|||||||
|
|
||||||
SendWXDebugInfo("Model Name: "+sModelName);
|
SendWXDebugInfo("Model Name: "+sModelName);
|
||||||
|
|
||||||
$('#ModelName').html(sModelName);
|
$('#ModelName').text(sModelName);
|
||||||
$('#ModelName').attr('title',sModelName);
|
$('#ModelName').attr('title',sModelName);
|
||||||
$('#ModelAuthorName').html(sModelAuthor);
|
$('#ModelAuthorName').text(sModelAuthor);
|
||||||
|
|
||||||
switch(UploadType)
|
switch(UploadType)
|
||||||
{
|
{
|
||||||
@@ -268,7 +268,7 @@ function ShowModelInfo( pModel )
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#Model_Desc').html( html_decode(sModelDesc) );
|
$('#Model_Desc').empty().append( SanitizeDescHtml( html_decode(sModelDesc) ) );
|
||||||
|
|
||||||
let ModelPreviewList=pModel.preview_img;
|
let ModelPreviewList=pModel.preview_img;
|
||||||
let TotalPreview=ModelPreviewList.length;
|
let TotalPreview=ModelPreviewList.length;
|
||||||
@@ -410,7 +410,8 @@ function ConstructFileHtml( ID, pItem )
|
|||||||
{
|
{
|
||||||
let fTotal=pItem.length;
|
let fTotal=pItem.length;
|
||||||
|
|
||||||
let strHtml='';
|
let pBoard=$('#'+ID+' .FileListBoard');
|
||||||
|
pBoard.empty();
|
||||||
for( let f=0;f<fTotal;f++ )
|
for( let f=0;f<fTotal;f++ )
|
||||||
{
|
{
|
||||||
let pOne=pItem[f];
|
let pOne=pItem[f];
|
||||||
@@ -443,39 +444,92 @@ function ConstructFileHtml( ID, pItem )
|
|||||||
ImgPath='img/default.png';
|
ImgPath='img/default.png';
|
||||||
}
|
}
|
||||||
|
|
||||||
//Add html
|
//Add html. File names come from the 3MF, so build the nodes rather than concatenating markup.
|
||||||
|
let pIconImg=$('<img />').attr('src',ImgPath);
|
||||||
|
let pMenu=$('<div class="FileMenu"><img src="img/s.svg" /></div>');
|
||||||
if( strClass!='ImageIcon' )
|
if( strClass!='ImageIcon' )
|
||||||
{
|
{
|
||||||
strHtml+='<div class="FileItem">'+
|
pMenu.on('click', function(){ OnClickOpenFile(tPath); });
|
||||||
' <div class="'+strClass+'"><img src="'+ImgPath+'" /></div>'+
|
|
||||||
' <div class="FileText">'+
|
|
||||||
' <div class="FileName">'+tName+'</div>'+
|
|
||||||
' </div>'+
|
|
||||||
' <div class="FileMenu" onClick="OnClickOpenFile(\''+tPath+'\')"><img src="img/s.svg" /></div>'+
|
|
||||||
'</div>';
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ImgID++;
|
ImgID++;
|
||||||
let TmpImgID="AF"+ImgID;
|
let TmpImgID="AF"+ImgID;
|
||||||
|
|
||||||
strHtml+='<div class="FileItem">'+
|
pIconImg.attr('id',TmpImgID);
|
||||||
' <div class="'+strClass+'"><img id="'+TmpImgID+'" src="'+ImgPath+'" /></div>'+
|
pMenu.on('click', function(){ OnClickOpenImage(TmpImgID); });
|
||||||
' <div class="FileText">'+
|
|
||||||
' <div class="FileName">'+tName+'</div>'+
|
|
||||||
' </div>'+
|
|
||||||
' <div class="FileMenu" onClick="OnClickOpenImage(\''+TmpImgID+'\')"><img src="img/s.svg" /></div>'+
|
|
||||||
'</div>';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#'+ID+' .FileListBoard').html(strHtml);
|
let pFileItem=$('<div class="FileItem"></div>');
|
||||||
|
pFileItem.append( $('<div></div>').addClass(strClass).append(pIconImg) );
|
||||||
|
pFileItem.append( $('<div class="FileText"></div>').append( $('<div class="FileName"></div>').text(tName) ) );
|
||||||
|
pFileItem.append( pMenu );
|
||||||
|
pBoard.append( pFileItem );
|
||||||
|
}
|
||||||
|
|
||||||
if( fTotal>0 )
|
if( fTotal>0 )
|
||||||
$('#'+ID).show();
|
$('#'+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 )
|
function ShowProfilelInfo( pProfile )
|
||||||
{
|
{
|
||||||
//==========Profile Info==========
|
//==========Profile Info==========
|
||||||
@@ -483,10 +537,10 @@ function ShowProfilelInfo( pProfile )
|
|||||||
let sProfileAuthor=decodeURIComponent(pProfile.author);
|
let sProfileAuthor=decodeURIComponent(pProfile.author);
|
||||||
let sProfileDesc=decodeURIComponent(pProfile.description);
|
let sProfileDesc=decodeURIComponent(pProfile.description);
|
||||||
|
|
||||||
$('#ProfileName').html(sProfileName);
|
$('#ProfileName').text(sProfileName);
|
||||||
$('#ProfileAuthor').html(sProfileAuthor);
|
$('#ProfileAuthor').text(sProfileAuthor);
|
||||||
|
|
||||||
$('#Profile_Desc').html( html_decode(sProfileDesc) );
|
$('#Profile_Desc').empty().append( SanitizeDescHtml( html_decode(sProfileDesc) ) );
|
||||||
|
|
||||||
let ProfilePreviewList=pProfile.preview_img;
|
let ProfilePreviewList=pProfile.preview_img;
|
||||||
let TotalPreview=ProfilePreviewList.length;
|
let TotalPreview=ProfilePreviewList.length;
|
||||||
|
|||||||
@@ -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
|
// 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.
|
// 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);
|
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
|
// Orca: custom protocal support utils
|
||||||
inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); }
|
inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); }
|
||||||
|
|||||||
@@ -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);
|
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)
|
bool is_img_file(const std::string &path)
|
||||||
{
|
{
|
||||||
return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg");
|
return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg");
|
||||||
|
|||||||
@@ -293,9 +293,13 @@ void ProjectPanel::OnScriptMessage(wxWebViewEvent& evt)
|
|||||||
if (!accessory_path.empty()) {
|
if (!accessory_path.empty()) {
|
||||||
std::string decode_path = wxGetApp().url_decode(accessory_path.ToStdString());
|
std::string decode_path = wxGetApp().url_decode(accessory_path.ToStdString());
|
||||||
fs::path path(decode_path);
|
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);
|
wxLaunchDefaultApplication(path.wstring(), 0);
|
||||||
|
} else {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "open_3mf_accessory: ignoring path outside the project auxiliary directory: " << decode_path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
CHECK(is_symlink_target_within_root("link", "in/lib.so", root));
|
||||||
}
|
}
|
||||||
#endif
|
#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
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user