mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +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:
@@ -50,7 +50,9 @@ from src.web_interface.validators import (
|
||||
validate_file_upload
|
||||
)
|
||||
from src.common.permission_utils import install_requirements_file
|
||||
from src.common.path_safety import resolve_under
|
||||
from src.common.path_safety import resolve_under, safe_path_component
|
||||
from src.core_config_keys import CORE_CONFIG_KEYS, CORE_SECRETS_KEYS
|
||||
from src.backup_manager import BUNDLED_FONTS as _BUNDLED_FONTS
|
||||
from src.device_location import DeviceLocationResolver, apply_device_location
|
||||
_SUDO = shutil.which('sudo')
|
||||
_JOURNALCTL = shutil.which('journalctl')
|
||||
@@ -112,7 +114,15 @@ SYSTEM_FONTS = frozenset([
|
||||
'10x20',
|
||||
'matrixchunky8', 'matrixlight6', 'tom-thumb',
|
||||
'clr6x12', 'helvr12', 'texgyre-27'
|
||||
])
|
||||
]) | frozenset(
|
||||
# Every font the repository ships, from the list backups already keep in
|
||||
# sync with assets/fonts/. The hand-written names above had drifted from
|
||||
# it (MatrixChunky8X, MatrixLight6X, MatrixLight8X and ic8x8u were
|
||||
# missing), so DELETE /fonts/<name> removed git-tracked fonts. Keys are
|
||||
# the lowercased file stem, which is what the catalog and delete compare.
|
||||
os.path.splitext(_name)[0].lower() for _name in _BUNDLED_FONTS
|
||||
if _name.lower().endswith(('.ttf', '.otf', '.bdf'))
|
||||
)
|
||||
api_v3 = Blueprint('api_v3', __name__)
|
||||
|
||||
|
||||
@@ -592,6 +602,27 @@ def _installed_plugin_ids():
|
||||
instead of relying on the tracker's in-memory `get_all_*` view.
|
||||
"""
|
||||
return list(_discovered_plugin_manifests())
|
||||
def _non_plugin_id_error(plugin_id):
|
||||
"""Error response when ``plugin_id`` cannot name a plugin, else None.
|
||||
|
||||
Uninstall and config reset take the id from the request body and delete
|
||||
or overwrite ``config[plugin_id]`` -- so ``{"plugin_id": "display"}``
|
||||
removed the core display section and reported success. Core sections are
|
||||
never plugin ids. The secrets-only core keys (``github`` holds the Plugin
|
||||
Store token) are refused too, unless a plugin by that id is really
|
||||
installed. Uninstall deliberately does not require the plugin to be
|
||||
installed: it must still clean the config of one whose directory is gone.
|
||||
"""
|
||||
if safe_path_component(plugin_id) is None:
|
||||
return error_response(ErrorCode.INVALID_INPUT,
|
||||
f'Invalid plugin id: {plugin_id!r}', status_code=400)
|
||||
if plugin_id in CORE_CONFIG_KEYS or (
|
||||
plugin_id in CORE_SECRETS_KEYS
|
||||
and plugin_id not in _discovered_plugin_manifests(plugin_id)):
|
||||
return error_response(ErrorCode.INVALID_INPUT,
|
||||
f"'{plugin_id}' is a core configuration section, not a plugin",
|
||||
status_code=400)
|
||||
return None
|
||||
def _discovered_plugin_manifests(plugin_id=None, rescan=False):
|
||||
"""The plugin manager's manifests, discovering plugins first if needed.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user