If plugin_dir somehow resolves to '/' or a bare drive root, os.path.basename()
returns '', causing safe_plugin_dir to equal plugins_dir_real and the isdir()
check to pass incorrectly. Reject early with a clear error in that case.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Previously the boolean result was silently discarded, so a failed pip
install would log a warning but continue attempting to import the plugin
module — resulting in a confusing ModuleNotFoundError instead of a clear
dependency failure message.
Now raises PluginError with plugin_id and plugin_dir if dependency
installation fails, stopping the load before the import is attempted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace relative_to() (not recognised by CodeQL as a path sanitiser) with
the os.path.realpath() + startswith() pattern that CodeQL explicitly models
as sanitising py/path-injection.
- Add plugins_dir optional param to install_dependencies() and load_plugin()
- PluginManager.load_plugin() passes self.plugins_dir as the trusted anchor;
install_dependencies() validates that the resolved plugin_dir starts with
the resolved plugins_dir before any file I/O
- Replace all Path.read_bytes/read_text/write_text/exists with open() and
os.path.isfile() so the sanitised string paths flow directly to file ops
without re-introducing taint through Path object conversion
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add explicit relative_to() containment check after path resolution so
CodeQL recognizes the plugin directory boundary (fixes 4 CodeQL alerts:
Uncontrolled data used in path expression, lines 168/172/189/205)
- Wrap requirements_file.read_bytes() in try/except OSError — on Raspberry
Pi with flaky SD card storage this can fail; returns False with a clear log
- Wrap marker_path.read_text() in try/except OSError — a corrupted marker
falls through to a clean reinstall instead of crashing
- Wrap both marker_path.write_text() calls in try/except OSError — pip
already succeeded at this point so a marker write failure should not
return False or propagate through the generic exception handler
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The .dependencies_installed marker was an empty file, so adding a new
package to requirements.txt (e.g. astral in ledmatrix-weather v2.3.0)
never triggered a pip re-install on existing installs — the file existed
so the check returned early.
The marker now stores a SHA-256 hash of requirements.txt. On every plugin
load, the loader compares the current hash to the stored one; a mismatch
(or missing marker) triggers pip install and writes the new hash.
store_manager._install_dependencies() also writes the hash marker after a
store install/update so the loader skips a redundant pip run on next boot.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>