mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
fix(security): refuse unsafe plugin ids, keep secrets private, validate request bodies (#643)
* fix(security): refuse unsafe plugin ids, keep secrets private, validate bodies - install_from_url and the registry install's manifest rename refuse a plugin id that is not a single safe name (no ../ out of plugins_dir). - Uninstall and config reset refuse core config sections and ids with path parts; uninstall of a plugin whose directory is gone still works. - separate_secrets checks a field's own x-secret marker before recursing, so object/array secrets no longer land in config.json. - Backup restore creates missing secrets/wifi/ytm files with mode 640; export skips non-object manifests and no longer collides on same-second exports. - SYSTEM_FONTS includes every bundled font from BUNDLED_FONTS. - Raw config/secrets saves and validate_request_json require a JSON object. - A blank max_dynamic_duration_seconds keeps the stored value; other values are validated to 30-1800 instead of raising a 500. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(security): validate the id before install_plugin moves anything; claim backup names atomically - install_plugin set aside plugins_dir / plugin_id before any id check, so "../x" moved a directory outside the plugins dir (the rollback moved it back, but only if the install path got that far) - two exports finishing in the same second could both see a free name and the later os.replace destroyed the first archive; the name is now claimed with O_EXCL before the archive is swapped in Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1178,6 +1178,13 @@ class PluginStoreManager:
|
||||
other's freshly installed copy. The lock is reentrant because the
|
||||
rollback path already holds it when it calls in here.
|
||||
"""
|
||||
# Before anything touches the filesystem: plugin_id comes from the
|
||||
# request body, and the set-aside below moves plugins_dir / plugin_id
|
||||
# -- which for "../x" is a directory outside the plugins directory.
|
||||
if not self._is_valid_plugin_id(plugin_id):
|
||||
self.logger.error(f"Refusing to install invalid plugin id: {plugin_id!r}")
|
||||
return False
|
||||
|
||||
with self._get_reinstall_lock(plugin_id):
|
||||
plugin_path = self.plugins_dir / plugin_id
|
||||
if not plugin_path.exists():
|
||||
@@ -1345,6 +1352,13 @@ class PluginStoreManager:
|
||||
self.logger.error("Plugin manifest missing 'id' field")
|
||||
self._safe_remove_directory(plugin_path)
|
||||
return False
|
||||
# The manifest id becomes a directory name below (and the old
|
||||
# directory is removed to make room), so a downloaded manifest
|
||||
# saying "../x" must not steer that outside plugins_dir.
|
||||
if not self._is_valid_plugin_id(manifest_plugin_id):
|
||||
self.logger.error(f"Plugin manifest has an invalid 'id': {manifest_plugin_id!r}")
|
||||
self._safe_remove_directory(plugin_path)
|
||||
return False
|
||||
|
||||
# If manifest ID doesn't match directory name, rename directory to match manifest
|
||||
if manifest_plugin_id != plugin_id:
|
||||
@@ -1524,6 +1538,14 @@ class PluginStoreManager:
|
||||
'success': False,
|
||||
'error': 'No plugin ID found in manifest'
|
||||
}
|
||||
# plugin_id names the directory that is removed and then replaced
|
||||
# below, and it comes from the request body or a downloaded
|
||||
# manifest -- so "../x" would reach outside plugins_dir.
|
||||
if not self._is_valid_plugin_id(plugin_id):
|
||||
return {
|
||||
'success': False,
|
||||
'error': f'Invalid plugin ID: {plugin_id!r}'
|
||||
}
|
||||
|
||||
# Validate manifest has required fields
|
||||
required_fields = ['id', 'name', 'class_name', 'display_modes']
|
||||
|
||||
Reference in New Issue
Block a user