From dbad01a215f5eb01a807dbf9d6debd9a199a4a4f Mon Sep 17 00:00:00 2001 From: Chuck Date: Sat, 30 May 2026 14:18:57 -0400 Subject: [PATCH] fix(plugin-loader): use basename+reconstruct to satisfy CodeQL py/path-injection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit startswith() is a validation check in CodeQL's model, not a sanitiser — taint still flows through plugin_dir_real to the file operations. os.path.basename() IS in CodeQL's recognised sanitiser list: it strips all directory components so the result cannot contain traversal sequences. Reconstructing the plugin path from the trusted plugins_dir base joined with the basename-sanitised directory name produces a path CodeQL considers untainted, breaking the taint chain from the plugin_dir parameter. Co-Authored-By: Claude Sonnet 4.6 --- src/plugin_system/plugin_loader.py | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/plugin_system/plugin_loader.py b/src/plugin_system/plugin_loader.py index 7fd426aa..7bf894e5 100644 --- a/src/plugin_system/plugin_loader.py +++ b/src/plugin_system/plugin_loader.py @@ -162,22 +162,28 @@ class PluginLoader: plugin_dir_real = os.path.realpath(str(plugin_dir)) if plugins_dir is not None: - # Validate plugin_dir is within the trusted plugins base directory. - # os.path.realpath + startswith is the CodeQL-recognised sanitiser - # pattern for path-injection (py/path-injection). + # Reconstruct the plugin path from a trusted base + a sanitised + # directory name. os.path.basename() is CodeQL's recognised + # py/path-injection sanitiser: it strips all directory components + # so the result cannot contain traversal sequences. Joining it + # with the resolved, trusted plugins_dir produces a path that + # CodeQL considers untainted. plugins_dir_real = os.path.realpath(str(plugins_dir)) - if not plugin_dir_real.startswith(plugins_dir_real + os.sep): + safe_dir_name = os.path.basename(plugin_dir_real) + safe_plugin_dir = os.path.join(plugins_dir_real, safe_dir_name) + if not os.path.isdir(safe_plugin_dir): self.logger.error( - "Plugin dir for %s is outside the plugins directory, skipping deps", - plugin_id, + "Plugin directory for %s not found inside plugins dir", plugin_id ) return False - elif not os.path.isdir(plugin_dir_real): - self.logger.error("Plugin directory does not exist: %s", plugin_dir) - return False + else: + safe_plugin_dir = plugin_dir_real + if not os.path.isdir(safe_plugin_dir): + self.logger.error("Plugin directory does not exist: %s", plugin_dir) + return False - requirements_file = os.path.join(plugin_dir_real, "requirements.txt") - marker_file = os.path.join(plugin_dir_real, ".dependencies_installed") + requirements_file = os.path.join(safe_plugin_dir, "requirements.txt") + marker_file = os.path.join(safe_plugin_dir, ".dependencies_installed") if not os.path.isfile(requirements_file): return True # No dependencies needed