diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp index 8dd00935f7..dcbf638483 100644 --- a/src/libslic3r/Format/bbs_3mf.cpp +++ b/src/libslic3r/Format/bbs_3mf.cpp @@ -100,6 +100,45 @@ struct ZipUnicodePathExtraField } }; +// Validate that a relative file path does not escape the root directory via path traversal. +static bool is_path_within_root(const std::string& file_path, const boost::filesystem::path& root) +{ + if (file_path.empty()) + return false; + + boost::filesystem::path p(file_path); + if (p.is_absolute()) + return false; + + // Reject any path component that is ".." + for (const auto& component : p) { + if (component == "..") + return false; + } + + // Resolve the full path and verify it starts with the canonical root (also catches symlink escapes) + try { + boost::filesystem::path full_path = root / p; + boost::filesystem::path canonical_root = boost::filesystem::weakly_canonical(root); + boost::filesystem::path canonical_full = boost::filesystem::weakly_canonical(full_path); + + auto root_str = canonical_root.string(); + auto full_str = canonical_full.string(); + if (full_str.length() < root_str.length()) + return false; + if (full_str.compare(0, root_str.length(), root_str) != 0) + return false; + // Ensure it's a proper prefix (not just a substring of a longer directory name) + if (full_str.length() > root_str.length() && + full_str[root_str.length()] != boost::filesystem::path::preferred_separator) + return false; + } catch (const boost::filesystem::filesystem_error&) { + return false; + } + + return true; +} + // VERSION NUMBERS // 0 : .3mf, files saved by older slic3r or other applications. No version definition in them. // 1 : Introduction of 3mf versioning. No other change in data saved into 3mf files. @@ -1774,8 +1813,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" << __LINE__ << boost::format("extract %1%th file %2%, total=%3%")%(i+1)%name%num_entries; - if (name.find("/../") != std::string::npos) { - BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", find file path including /../, not valid, skip it\n"); + if (name.find("/../") != std::string::npos || + name.find("../") == 0 || + (name.length() >= 3 && name.compare(name.length() - 3, 3, "/..") == 0) || + name == "..") { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", find file path including path traversal, not valid, skip it\n"); continue; } @@ -2649,6 +2691,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) else return; + if (!is_path_within_root(dest_file, dir)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", path traversal detected in auxiliary file: %1%, skipping") % dest_file; + return; + } + if (dest_file.find('/') != std::string::npos) { boost::filesystem::path src_path = boost::filesystem::path(dest_file); boost::filesystem::path parent_path = src_path.parent_path(); @@ -2672,6 +2719,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result) { if (stat.m_uncomp_size > 0) { std::string src_file = decode_path(stat.m_filename); + + boost::filesystem::path backup_root(m_backup_path); + if (!is_path_within_root(src_file, backup_root)) { + BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(", path traversal detected in file: %1%, skipping") % src_file; + return; + } + // BBS: use backup path //aux directory from model boost::filesystem::path dest_path = boost::filesystem::path(m_backup_path + "/" + src_file);