From 721a3eb8bb9e389ddb283975aa7d9a49dd9267f1 Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Thu, 8 Oct 2026 14:46:54 +0800 Subject: [PATCH] Judge Plugin Audit Keywords Only Below the Allowed Root The audit hook denies any path with a component containing secret, cert or conf, before the allowed roots are considered. On Linux the data directory is ~/.config/OrcaSlicer, so every file a plugin touched under its own data folder from a plugin callback was denied. Judge only the components below the deepest allowed root that holds the path: the root's own components are the host's choice, while a keyword below it still denies, which keeps the bundled certificate folder unreachable. --- src/slic3r/plugin/PluginAuditManager.cpp | 69 ++++++++++++++---------- tests/slic3rutils/test_plugin_audit.cpp | 38 +++++++++++++ 2 files changed, 80 insertions(+), 27 deletions(-) diff --git a/src/slic3r/plugin/PluginAuditManager.cpp b/src/slic3r/plugin/PluginAuditManager.cpp index 1fe8837c5c..e5aff094e6 100644 --- a/src/slic3r/plugin/PluginAuditManager.cpp +++ b/src/slic3r/plugin/PluginAuditManager.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -144,29 +145,28 @@ static bool is_fs_category(AuditEventCategory category) // Path safety // --------------------------------------------------------------------------- +// weakly_canonical resolves symlinks but does NOT require the path to exist: it canonicalizes +// the prefix that exists and appends the non-existing tail lexically. Falls back to an absolute, +// lexically normal path when even that fails. +static boost::filesystem::path canonical_or_normal(const boost::filesystem::path& path) +{ + namespace fs = boost::filesystem; + boost::system::error_code ec; + fs::path canon = fs::weakly_canonical(path, ec); + if (ec) { + canon = fs::absolute(path, ec).lexically_normal(); + if (ec) + canon = path; + } + return canon; +} + bool is_inside_allowed_root(const boost::filesystem::path& candidate, const boost::filesystem::path& allowed_root) { namespace fs = boost::filesystem; - boost::system::error_code ec; - - // Canonicalize both paths. weakly_canonical resolves symlinks but does - // NOT require the path to exist — it canonicalizes the prefix that exists - // and appends the non-existing tail lexically. - fs::path canon_candidate = fs::weakly_canonical(candidate, ec); - if (ec) { - // Fall back to lexically_normal + absolute - canon_candidate = fs::absolute(candidate, ec).lexically_normal(); - if (ec) - canon_candidate = candidate; - } - - fs::path canon_root = fs::weakly_canonical(allowed_root, ec); - if (ec) { - canon_root = fs::absolute(allowed_root, ec).lexically_normal(); - if (ec) - canon_root = allowed_root; - } + fs::path canon_candidate = canonical_or_normal(candidate); + fs::path canon_root = canonical_or_normal(allowed_root); // Component-wise comparison: the root must be a prefix of candidate, // and the next component must not be ".." or missing. @@ -354,19 +354,34 @@ bool PluginAuditManager::is_denied_path_keyword(const boost::filesystem::path& c { namespace fs = boost::filesystem; - boost::system::error_code ec; - fs::path canon = fs::weakly_canonical(candidate, ec); - if (ec) { - canon = fs::absolute(candidate, ec).lexically_normal(); - if (ec) - canon = candidate; - } + fs::path canon = canonical_or_normal(candidate); std::lock_guard lock(m_mutex); if (m_denied_path_keywords.empty()) return false; - for (const auto& component : canon) { + // Only the part of the path the plugin chose is judged. The components of the allowed root it + // sits in are the host's (on Linux the data directory is ~/.config/OrcaSlicer), so a keyword + // there must not deny everything underneath; a keyword below the root still does, which keeps + // resources_dir()/cert/... unreachable. + fs::path below = canon; + size_t depth = 0; + auto consider = [&](const AllowedRoot& root) { + if (!is_inside_allowed_root(canon, root.path)) + return; + const fs::path canon_root = canonical_or_normal(root.path); + const size_t root_depth = static_cast(std::distance(canon_root.begin(), canon_root.end())); + if (root_depth > depth) { + depth = root_depth; + below = canon.lexically_relative(canon_root); + } + }; + for (const auto& root : m_scoped_allowed_roots) + consider(root); + for (const auto& root : m_global_allowed_roots) + consider(root); + + for (const auto& component : below) { std::string name = component.string(); if (name.empty()) continue; diff --git a/tests/slic3rutils/test_plugin_audit.cpp b/tests/slic3rutils/test_plugin_audit.cpp index ca8c46fabd..405b7ddae1 100644 --- a/tests/slic3rutils/test_plugin_audit.cpp +++ b/tests/slic3rutils/test_plugin_audit.cpp @@ -253,6 +253,44 @@ TEST_CASE("Plugin audit denies secret/certificate/config-like paths by keyword", } } +TEST_CASE("Plugin audit keyword deny ignores the allowed root's own path", "[audit]") +{ + seed_denied_names(); + seed_denied_keywords(); + PluginAuditManager& mgr = PluginAuditManager::instance(); + + // The data directory of a Linux install: its own path carries the "conf" keyword. + const fs::path root = fs::temp_directory_path() / "plugin-audit-xdg" / ".config" / "OrcaSlicer"; + mgr.add_global_allowed_root(root.string()); + + SECTION("a plain file below the root is not denied by the root's components") + { + CHECK_FALSE(mgr.is_denied_path_keyword(root / "log" / "debug.log")); + CHECK_FALSE(mgr.is_denied_path_keyword(root / "orca_plugins" / "plugin_data" / "key" / "state.json")); + } + + SECTION("a keyword below the root still denies") + { + CHECK(mgr.is_denied_path_keyword(root / "cert" / "ca.pem")); + CHECK(mgr.is_denied_path_keyword(root / "plugin" / "secrets" / "token.txt")); + CHECK(mgr.is_denied_path_keyword(root / "plugin.conf")); + } + + SECTION("a keyword in a path outside every root still denies") + { + CHECK(mgr.is_denied_path_keyword(fs::path("/elsewhere/.config/app/file.txt"))); + } + + SECTION("inside a plugin context the log is readable and the app config stays blocked") + { + ScopedPluginAuditContext ctx("test_plugin", ""); + CHECK(mgr.check_open((root / "log" / "debug.log").string(), "r").allowed); + AuditDecision decision = mgr.check_open((root / (SLIC3R_APP_KEY ".conf")).string(), "r"); + CHECK_FALSE(decision.allowed); + CHECK(decision.reason == "denied filename"); + } +} + TEST_CASE("Plugin audit is_denied_path combines the filename and keyword registries", "[audit]") { seed_denied_names();