CodeQL flagged 2 new high-severity "uncontrolled data used in path
expression" alerts at the open() calls inside this PR's new
requirements_has_real_deps()/requirements_are_satisfied() -- both are
reachable from paths that were never run through the basename+trusted-base
sanitiser this codebase already uses elsewhere:
- PluginLoader.install_dependencies() only applied that sanitiser when its
optional plugins_dir argument was actually passed; the "no plugins_dir"
branch trusted plugin_dir_real directly. Made plugins_dir required (not
Optional) so that branch can't exist, and added an explicit guard in
load_plugin() so install_deps=True without a plugins_dir fails loudly
instead of silently. Production's only real caller (PluginManager) always
passes plugins_dir already; the harness/dev-server/render-plugin callers
all use install_deps=False and are unaffected.
- StoreManager._install_dependencies() never sanitised plugin_path at all,
and its call sites ultimately derive that path from a plugin's own
manifest.json "id" field (install_plugin_from_url) -- a malicious plugin
could otherwise point requirements_file outside plugins_dir. Applied the
same os.path.basename()-based containment pattern PluginLoader already
uses (and that CodeQL recognises as a real sanitiser).
Added test_install_dependencies_requires_plugins_dir and
test_install_dependencies_rejects_path_outside_plugins_dir to lock in the
actual security property, not just quiet the scanner. Verified: all 20
tests in test_plugin_loader.py pass, plus the PR's existing test plan
(test_plugin_system.py, test_store_manager_caches.py: 53 passed) and the
full CI plugin-safety suite (test_harness.py, test_visual_rendering.py,
test_plugin_matrix.py: 52 passed, 2 pre-existing skips) all still pass.
The .dependencies_installed hash-marker system only tracked "was this exact
requirements.txt hashed before" — not whether the packages it names are
actually present. That made it fragile (a wiped venv, a manually removed
package, or a lost/corrupted marker forces a needless full pip reinstall or,
worse, a false skip) and produced dead weight for the ~10 plugins whose
requirements.txt is comment-only (they still paid a pip subprocess on first
boot before a marker existed).
Replace it with requirements_are_satisfied() in plugin_loader.py, which
checks each real requirement line against importlib.metadata directly, so
install_dependencies() only shells out to pip when something is actually
missing or version-mismatched. Drops the marker file entirely: removed all
marker read/write sites in plugin_loader.py and store_manager.py, the
now-pointless marker-cleanup step in the git-update path, the unused legacy
marker implementation in plugin_manager.py, and the already-stale
clear_dependency_markers.sh script.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ