Plugin audit (#14821)

* Block plugins from reading or writing app config and cloud credentials

Add a denied-filename registry to the plugin audit sandbox, seeded with OrcaSlicer's config (.conf/.ini) and the cloud refresh-token file. The deny is checked above the loading-mode read exemption and the allowed roots, so a plugin cannot reach these files even though they sit inside data_dir(), which is itself an allowed root. Case-insensitive prefix matching also covers the .bak/.tmp companions that hold the same data, and os.rename/os.remove are hooked alongside open so the files cannot be deleted or clobbered either.
This commit is contained in:
SoftFever
2026-07-18 01:24:44 +08:00
committed by GitHub
parent 2717c37f63
commit 88fb89b5eb
6 changed files with 382 additions and 24 deletions

View File

@@ -52,6 +52,27 @@ public:
void add_global_allowed_root(const boost::filesystem::path& root);
void add_scoped_allowed_root(const boost::filesystem::path& root);
// --- denied-filenames registry ---
// Filenames a plugin may never touch, in any directory, regardless of audit mode or
// enclosing allowed root. A candidate is denied when its filename starts with a
// registered name, so .bak/.tmp companions are covered by the same entry.
//
// The comparison is case-insensitive on every platform, unlike the _WIN32-only iequals
// in is_inside_allowed_root: the default macOS APFS configuration is case-insensitive
// too, so `orcaslicer.conf` reaches the real file there. Over-blocking a genuinely
// distinct name on Linux is the fail-safe direction and costs nothing real.
void add_denied_filename(const std::string& filename);
// The list install_hook() seeds into the deny registry: the app config (both app keys and
// both extensions) and the cloud refresh token. Exposed so tests seed the exact same set
// without a live interpreter, so the test and production seeding cannot drift apart.
static std::vector<std::string> default_denied_filenames();
// True when candidate's base name starts with a denied name (case-insensitive). No path
// resolution: laundering a denied file through a symlink, hardlink, subprocess, or Windows
// 8.3 short name is out of scope (see the design doc). This blocks direct access only.
bool is_denied_filename(const boost::filesystem::path& candidate) const;
// --- enforcement mode ---
enum class AuditMode {
// Import/loading phase: allow reads anywhere, only block writes
@@ -68,6 +89,11 @@ public:
AuditMode audit_mode() const;
// --- policy checks ---
// Shared core for every audited filesystem event. The deny list is consulted above the
// Loading-mode read exemption and above the allowed roots, so a denied filename is
// blocked even though every scope currently runs in Loading and the files in question
// sit inside data_dir(), which is itself a global allowed root.
AuditDecision check_path_access(const boost::filesystem::path& candidate, bool is_write);
AuditDecision check_open(const std::string& path, const std::string& mode);
void report_violation(const AuditViolation& violation);
@@ -90,8 +116,10 @@ private:
static thread_local bool m_has_last_violation;
static thread_local AuditViolation m_last_violation;
std::mutex m_mutex;
// mutable: is_denied_filename() is a const query that must lock.
mutable std::mutex m_mutex;
std::vector<boost::filesystem::path> m_global_allowed_roots;
std::vector<std::string> m_denied_filenames;
};
// RAII guard that sets the current plugin key and capability name, restoring the previous