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:
@@ -129,7 +129,17 @@ def validate_request_json(required_fields: list, data: Optional[Dict] = None) ->
|
||||
"Request body must be valid JSON",
|
||||
status_code=400
|
||||
)
|
||||
|
||||
|
||||
# A JSON array passes the check above, and ``field in data`` then tests
|
||||
# list membership: ["plugin_id"] "had" every required field and the
|
||||
# handler's data['plugin_id'] raised TypeError -- a 500, not a 400.
|
||||
if not isinstance(data, dict):
|
||||
return None, error_response(
|
||||
ErrorCode.INVALID_INPUT,
|
||||
"Request body must be a JSON object",
|
||||
status_code=400
|
||||
)
|
||||
|
||||
missing_fields = [field for field in required_fields if field not in data]
|
||||
if missing_fields:
|
||||
return None, error_response(
|
||||
|
||||
@@ -64,7 +64,14 @@ def separate_secrets(
|
||||
secrets: Dict[str, Any] = {}
|
||||
for key, value in config.items():
|
||||
full_path = f"{prefix}.{key}" if prefix else key
|
||||
if isinstance(value, dict):
|
||||
# The field's own x-secret marker is checked before its type. A secret
|
||||
# whose value is an object or array used to fall into the recursion
|
||||
# below, where none of its children are marked, and was written to
|
||||
# config.json in plain text. mask_secret_fields already checks the
|
||||
# marker first, so this also matches what the API masks.
|
||||
if full_path in secret_paths:
|
||||
secrets[key] = value
|
||||
elif isinstance(value, dict):
|
||||
nested_regular, nested_secrets = separate_secrets(value, secret_paths, full_path)
|
||||
if nested_regular:
|
||||
regular[key] = nested_regular
|
||||
@@ -94,8 +101,6 @@ def separate_secrets(
|
||||
secrets[key] = sec_items
|
||||
else:
|
||||
regular[key] = value
|
||||
elif full_path in secret_paths:
|
||||
secrets[key] = value
|
||||
else:
|
||||
regular[key] = value
|
||||
return regular, secrets
|
||||
|
||||
Reference in New Issue
Block a user