fix(starlark): code review fixes - security, robustness, and schema parsing

## Security Fixes
- manager.py: Check _update_manifest_safe return values to prevent silent failures
- manager.py: Improve temp file cleanup in _save_manifest to prevent leaks
- manager.py: Fix uninstall order (manifest → memory → disk) for consistency
- api_v3.py: Add path traversal validation in uninstall endpoint
- api_v3.py: Implement atomic writes for manifest files with temp + rename
- pixlet_renderer.py: Relax config validation to only block dangerous shell metacharacters

## Frontend Robustness
- plugins_manager.js: Add safeLocalStorage wrapper for restricted contexts (private browsing)
- starlark_config.html: Scope querySelector to container to prevent modal conflicts

## Schema Parsing Improvements
- pixlet_renderer.py: Indentation-aware get_schema() extraction (handles nested functions)
- pixlet_renderer.py: Handle quoted defaults with commas (e.g., "New York, NY")
- tronbyte_repository.py: Validate file_name is string before path traversal checks

## Dependencies
- requirements.txt: Update Pillow (10.4.0), PyYAML (6.0.2), requests (2.32.0)

## Documentation
- docs/STARLARK_APPS_GUIDE.md: Comprehensive guide explaining:
  - How Starlark apps work
  - That apps come from Tronbyte (not LEDMatrix)
  - Installation, configuration, troubleshooting
  - Links to upstream projects

All changes improve security, reliability, and user experience.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-02-19 16:58:22 -05:00
parent 5d213b5747
commit 441b3c56e9
8 changed files with 667 additions and 43 deletions

View File

@@ -7078,14 +7078,29 @@ def _read_starlark_manifest() -> dict:
def _write_starlark_manifest(manifest: dict) -> bool:
"""Write the starlark-apps manifest.json to disk."""
"""Write the starlark-apps manifest.json to disk with atomic write."""
temp_file = None
try:
_STARLARK_APPS_DIR.mkdir(parents=True, exist_ok=True)
with open(_STARLARK_MANIFEST_FILE, 'w') as f:
# Atomic write pattern: write to temp file, then rename
temp_file = _STARLARK_MANIFEST_FILE.with_suffix('.tmp')
with open(temp_file, 'w') as f:
json.dump(manifest, f, indent=2)
f.flush()
os.fsync(f.fileno()) # Ensure data is written to disk
# Atomic rename (overwrites destination)
temp_file.replace(_STARLARK_MANIFEST_FILE)
return True
except OSError as e:
logger.error(f"Error writing starlark manifest: {e}")
# Clean up temp file if it exists
if temp_file and temp_file.exists():
try:
temp_file.unlink()
except Exception:
pass
return False
@@ -7398,7 +7413,15 @@ def uninstall_starlark_app(app_id):
else:
# Standalone: remove app dir and manifest entry
import shutil
app_dir = _STARLARK_APPS_DIR / app_id
app_dir = (_STARLARK_APPS_DIR / app_id).resolve()
# Path traversal check - ensure app_dir is within _STARLARK_APPS_DIR
try:
app_dir.relative_to(_STARLARK_APPS_DIR.resolve())
except ValueError:
logger.warning(f"Path traversal attempt in uninstall: {app_id}")
return jsonify({'status': 'error', 'message': 'Invalid app_id'}), 400
if app_dir.exists():
shutil.rmtree(app_dir)
manifest = _read_starlark_manifest()