fix(starlark): security and race condition improvements

Security fixes:
- Add path traversal validation for output_path in download_star_file
- Remove XSS-vulnerable inline onclick handlers, use delegated events
- Add type hints to helper functions for better type safety

Race condition fixes:
- Lock manifest file BEFORE creating temp file in _save_manifest
- Hold exclusive lock for entire read-modify-write cycle in _update_manifest_safe
- Prevent concurrent writers from racing on manifest updates

Other improvements:
- Fix pages_v3.py standalone mode to load config.json from disk
- Improve error handling with proper logging in cleanup blocks
- Add explicit type annotations to Starlark helper functions

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-02-19 19:37:09 -05:00
parent aafb238ac9
commit 6a60a57421
5 changed files with 147 additions and 39 deletions

View File

@@ -10,7 +10,7 @@ import uuid
import logging
from datetime import datetime
from pathlib import Path
from typing import Optional
from typing import Optional, Tuple, Dict, Any, Type
logger = logging.getLogger(__name__)
@@ -6980,7 +6980,7 @@ def clear_old_errors():
# ─── Starlark Apps API ──────────────────────────────────────────────────────
def _get_tronbyte_repository_class():
def _get_tronbyte_repository_class() -> Type[Any]:
"""Import TronbyteRepository from plugin-repos directory."""
import importlib.util
import importlib
@@ -7007,7 +7007,7 @@ def _get_tronbyte_repository_class():
return module.TronbyteRepository
def _get_pixlet_renderer_class():
def _get_pixlet_renderer_class() -> Type[Any]:
"""Import PixletRenderer from plugin-repos directory."""
import importlib.util
import importlib
@@ -7034,7 +7034,7 @@ def _get_pixlet_renderer_class():
return module.PixletRenderer
def _validate_and_sanitize_app_id(app_id, fallback_source=None):
def _validate_and_sanitize_app_id(app_id: Optional[str], fallback_source: Optional[str] = None) -> Tuple[Optional[str], Optional[str]]:
"""Validate and sanitize app_id to a safe slug."""
if not app_id and fallback_source:
app_id = fallback_source
@@ -7051,7 +7051,7 @@ def _validate_and_sanitize_app_id(app_id, fallback_source=None):
return sanitized, None
def _validate_timing_value(value, field_name, min_val=1, max_val=86400):
def _validate_timing_value(value: Any, field_name: str, min_val: int = 1, max_val: int = 86400) -> Tuple[Optional[int], Optional[str]]:
"""Validate and coerce timing values."""
if value is None:
return None, None
@@ -7066,7 +7066,7 @@ def _validate_timing_value(value, field_name, min_val=1, max_val=86400):
return int_value, None
def _get_starlark_plugin():
def _get_starlark_plugin() -> Optional[Any]:
"""Get the starlark-apps plugin instance, or None."""
if not api_v3.plugin_manager:
return None
@@ -7078,7 +7078,7 @@ _STARLARK_APPS_DIR = PROJECT_ROOT / 'starlark-apps'
_STARLARK_MANIFEST_FILE = _STARLARK_APPS_DIR / 'manifest.json'
def _read_starlark_manifest() -> dict:
def _read_starlark_manifest() -> Dict[str, Any]:
"""Read the starlark-apps manifest.json directly from disk."""
try:
if _STARLARK_MANIFEST_FILE.exists():
@@ -7089,7 +7089,7 @@ def _read_starlark_manifest() -> dict:
return {'apps': {}}
def _write_starlark_manifest(manifest: dict) -> bool:
def _write_starlark_manifest(manifest: Dict[str, Any]) -> bool:
"""Write the starlark-apps manifest.json to disk with atomic write."""
temp_file = None
try:
@@ -7116,7 +7116,7 @@ def _write_starlark_manifest(manifest: dict) -> bool:
return False
def _install_star_file(app_id: str, star_file_path: str, metadata: dict, assets_dir: Optional[str] = None) -> bool:
def _install_star_file(app_id: str, star_file_path: str, metadata: Dict[str, Any], assets_dir: Optional[str] = None) -> bool:
"""Install a .star file and update the manifest (standalone, no plugin needed)."""
import shutil
import json

View File

@@ -480,6 +480,16 @@ def _load_starlark_config_partial(app_id):
except Exception as e:
print(f"Warning: Could not load schema for {app_id}: {e}")
# Load config from config.json if it exists
config = {}
config_file = Path(__file__).resolve().parent.parent.parent / 'starlark-apps' / app_id / 'config.json'
if config_file.exists():
try:
with open(config_file, 'r') as f:
config = json.load(f)
except Exception as e:
print(f"Warning: Could not load config for {app_id}: {e}")
return render_template(
'v3/partials/starlark_config.html',
app_id=app_id,
@@ -487,7 +497,7 @@ def _load_starlark_config_partial(app_id):
app_enabled=app_data.get('enabled', True),
render_interval=app_data.get('render_interval', 300),
display_duration=app_data.get('display_duration', 15),
config=app_data.get('config', {}),
config=config,
schema=schema,
has_frames=False,
frame_count=0,

View File

@@ -7898,7 +7898,7 @@ setTimeout(function() {
grid.innerHTML = apps.map(app => {
const installed = isStarlarkInstalled(app.id);
return `
<div class="plugin-card">
<div class="plugin-card" data-app-id="${escapeHtml(app.id)}">
<div class="flex items-start justify-between mb-4">
<div class="flex-1 min-w-0">
<div class="flex items-center flex-wrap gap-1.5 mb-2">
@@ -7914,15 +7914,34 @@ setTimeout(function() {
</div>
</div>
<div class="flex gap-2 mt-auto pt-3 border-t border-gray-200">
<button onclick="window.installStarlarkApp('${escapeHtml(app.id)}')" class="btn ${installed ? 'bg-gray-500 hover:bg-gray-600' : 'bg-green-600 hover:bg-green-700'} text-white px-4 py-2 rounded-md text-sm font-semibold flex-1 flex justify-center items-center">
<button data-action="install" class="btn ${installed ? 'bg-gray-500 hover:bg-gray-600' : 'bg-green-600 hover:bg-green-700'} text-white px-4 py-2 rounded-md text-sm font-semibold flex-1 flex justify-center items-center">
<i class="fas ${installed ? 'fa-redo' : 'fa-download'} mr-2"></i>${installed ? 'Reinstall' : 'Install'}
</button>
<button onclick="window.open('https://github.com/tronbyt/apps/tree/main/apps/${encodeURIComponent(app.id)}', '_blank')" class="btn bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-semibold flex justify-center items-center">
<button data-action="view" class="btn bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md text-sm font-semibold flex justify-center items-center">
<i class="fas fa-external-link-alt mr-1"></i>View
</button>
</div>
</div>`;
}).join('');
// Add delegated event listeners for install and view buttons
grid.addEventListener('click', function(e) {
const button = e.target.closest('button[data-action]');
if (!button) return;
const card = button.closest('.plugin-card');
if (!card) return;
const appId = card.dataset.appId;
if (!appId) return;
const action = button.dataset.action;
if (action === 'install') {
window.installStarlarkApp(appId);
} else if (action === 'view') {
window.open('https://github.com/tronbyt/apps/tree/main/apps/' + encodeURIComponent(appId), '_blank');
}
});
}
// ── Filter UI Updates ───────────────────────────────────────────────────