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:
@@ -124,6 +124,23 @@ class TestSeparateSecrets:
|
||||
assert regular == {"accounts": [{"name": "a"}, "oddball"]}
|
||||
assert secrets == {"accounts": [{"token": "ta"}, {}]}
|
||||
|
||||
def test_secret_field_holding_an_object_or_array_goes_to_secrets(self):
|
||||
# x-secret on the field itself, not its children: the dict/list type
|
||||
# check used to win, and the whole value landed in config.json.
|
||||
props = {
|
||||
"oauth": {"type": "object", "x-secret": True},
|
||||
"cookies": {"type": "array", "x-secret": True},
|
||||
"city": {"type": "string"},
|
||||
}
|
||||
config = {"oauth": {"refresh": "r3fr3sh"}, "cookies": ["c1", "c2"],
|
||||
"city": "Austin"}
|
||||
regular, secrets = separate_secrets(config, find_secret_fields(props))
|
||||
assert regular == {"city": "Austin"}
|
||||
assert secrets == {"oauth": {"refresh": "r3fr3sh"}, "cookies": ["c1", "c2"]}
|
||||
# ...which is what the API already masks for those fields.
|
||||
masked = mask_secret_fields(config, props)
|
||||
assert masked["oauth"] == "" and masked["cookies"] == ""
|
||||
|
||||
def test_array_without_secret_paths_stays_regular(self):
|
||||
config = {"teams": ["DAL", "HOU"]}
|
||||
regular, secrets = separate_secrets(config, {"api_key"})
|
||||
|
||||
Reference in New Issue
Block a user