Files
LEDMatrix/test/test_api_v3_bundled_fonts_protected.py
ChuckandClaude Opus 5.5 bcef1957a9 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>
2026-09-28 08:24:43 -04:00

57 lines
2.0 KiB
Python

"""DELETE /fonts/<name> must refuse every font the repository ships.
The api's SYSTEM_FONTS was a hand-written list that had drifted from
backup_manager.BUNDLED_FONTS: MatrixChunky8X, MatrixLight6X, MatrixLight8X and
ic8x8u were missing, so deleting them removed git-tracked files (and the next
`git pull` either restored them or conflicted).
"""
import os
import sys
from pathlib import Path
from unittest.mock import patch
import pytest
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.backup_manager import BUNDLED_FONTS # noqa: E402
from test._api_v3_test_helpers import api_v3_client, api_v3_module # noqa: F401,E402
from web_interface.blueprints.api_v3 import SYSTEM_FONTS # noqa: E402
SHIPPED_FONT_FILES = sorted(
name for name in BUNDLED_FONTS if name.lower().endswith(('.ttf', '.otf', '.bdf')))
def test_every_bundled_font_is_a_system_font():
stems = {os.path.splitext(name)[0].lower() for name in SHIPPED_FONT_FILES}
assert stems <= SYSTEM_FONTS, stems - SYSTEM_FONTS
@pytest.fixture
def fonts_root(tmp_path):
fonts = tmp_path / "assets" / "fonts"
fonts.mkdir(parents=True)
with patch("web_interface.blueprints.api_v3.fonts.PROJECT_ROOT", tmp_path):
yield fonts
@pytest.mark.parametrize("filename", ["MatrixChunky8X.bdf", "MatrixLight6X.bdf",
"MatrixLight8X.bdf", "ic8x8u.bdf"])
def test_the_previously_unprotected_fonts_cannot_be_deleted(api_v3_client, fonts_root, filename):
(fonts_root / filename).write_text("BUNDLED")
response = api_v3_client.delete(f"/api/v3/fonts/{Path(filename).stem}")
assert response.status_code == 403
assert (fonts_root / filename).exists()
def test_a_user_font_can_still_be_deleted(api_v3_client, fonts_root):
(fonts_root / "my-upload.ttf").write_bytes(b"USER")
response = api_v3_client.delete("/api/v3/fonts/my-upload")
assert response.status_code == 200, response.get_json()
assert not (fonts_root / "my-upload.ttf").exists()