mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-04-10 21:03:01 +00:00
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:
@@ -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()
|
||||
|
||||
@@ -1,6 +1,43 @@
|
||||
// ─── LocalStorage Safety Wrappers ────────────────────────────────────────────
|
||||
// Handles environments where localStorage is unavailable or restricted (private browsing, etc.)
|
||||
const safeLocalStorage = {
|
||||
getItem(key) {
|
||||
try {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
return safeLocalStorage.getItem(key);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`safeLocalStorage.getItem failed for key "${key}":`, e.message);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
setItem(key, value) {
|
||||
try {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
safeLocalStorage.setItem(key, value);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`safeLocalStorage.setItem failed for key "${key}":`, e.message);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
removeItem(key) {
|
||||
try {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
localStorage.removeItem(key);
|
||||
return true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`localStorage.removeItem failed for key "${key}":`, e.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Define critical functions immediately so they're available before any HTML is rendered
|
||||
// Debug logging controlled by localStorage.setItem('pluginDebug', 'true')
|
||||
const _PLUGIN_DEBUG_EARLY = typeof localStorage !== 'undefined' && localStorage.getItem('pluginDebug') === 'true';
|
||||
// Debug logging controlled by safeLocalStorage.setItem('pluginDebug', 'true')
|
||||
const _PLUGIN_DEBUG_EARLY = safeLocalStorage.getItem('pluginDebug') === 'true';
|
||||
if (_PLUGIN_DEBUG_EARLY) console.log('[PLUGINS SCRIPT] Defining configurePlugin and togglePlugin at top level...');
|
||||
|
||||
// Expose on-demand functions early as stubs (will be replaced when IIFE runs)
|
||||
@@ -865,7 +902,7 @@ window.currentPluginConfig = null;
|
||||
|
||||
// Store filter/sort state
|
||||
const storeFilterState = {
|
||||
sort: localStorage.getItem('storeSort') || 'a-z',
|
||||
sort: safeLocalStorage.getItem('storeSort') || 'a-z',
|
||||
filterVerified: false,
|
||||
filterNew: false,
|
||||
filterInstalled: null, // null = all, true = installed only, false = not installed only
|
||||
@@ -873,7 +910,7 @@ window.currentPluginConfig = null;
|
||||
filterCategories: [],
|
||||
|
||||
persist() {
|
||||
localStorage.setItem('storeSort', this.sort);
|
||||
safeLocalStorage.setItem('storeSort', this.sort);
|
||||
},
|
||||
|
||||
reset() {
|
||||
@@ -898,7 +935,7 @@ window.currentPluginConfig = null;
|
||||
};
|
||||
|
||||
// Installed plugins sort state
|
||||
let installedSort = localStorage.getItem('installedSort') || 'a-z';
|
||||
let installedSort = safeLocalStorage.getItem('installedSort') || 'a-z';
|
||||
|
||||
// Shared on-demand status store (mirrors Alpine store when available)
|
||||
window.__onDemandStore = window.__onDemandStore || {
|
||||
@@ -1251,8 +1288,8 @@ const pluginLoadCache = {
|
||||
}
|
||||
};
|
||||
|
||||
// Debug flag - set via localStorage.setItem('pluginDebug', 'true')
|
||||
const PLUGIN_DEBUG = typeof localStorage !== 'undefined' && localStorage.getItem('pluginDebug') === 'true';
|
||||
// Debug flag - set via safeLocalStorage.setItem('pluginDebug', 'true')
|
||||
const PLUGIN_DEBUG = typeof localStorage !== 'undefined' && safeLocalStorage.getItem('pluginDebug') === 'true';
|
||||
function pluginLog(...args) {
|
||||
if (PLUGIN_DEBUG) console.log(...args);
|
||||
}
|
||||
@@ -5269,7 +5306,7 @@ function setupStoreFilterListeners() {
|
||||
installedSortSelect.value = installedSort;
|
||||
installedSortSelect.addEventListener('change', () => {
|
||||
installedSort = installedSortSelect.value;
|
||||
localStorage.setItem('installedSort', installedSort);
|
||||
safeLocalStorage.setItem('installedSort', installedSort);
|
||||
const plugins = window.installedPlugins || [];
|
||||
if (plugins.length > 0) {
|
||||
sortAndRenderInstalledPlugins(plugins);
|
||||
|
||||
@@ -155,13 +155,13 @@
|
||||
class="w-10 h-10 rounded border border-gray-300 cursor-pointer p-0.5"
|
||||
value="{{ current_val or '#FFFFFF' }}"
|
||||
data-starlark-color-picker="{{ field_id }}"
|
||||
oninput="document.querySelector('[data-starlark-config={{ field_id }}]').value = this.value">
|
||||
oninput="this.closest('.space-y-6').querySelector('[data-starlark-config={{ field_id }}]').value = this.value">
|
||||
<input type="text"
|
||||
class="form-control flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm font-mono"
|
||||
value="{{ current_val or '#FFFFFF' }}"
|
||||
placeholder="#RRGGBB"
|
||||
data-starlark-config="{{ field_id }}"
|
||||
oninput="var cp = document.querySelector('[data-starlark-color-picker={{ field_id }}]'); if(this.value.match(/^#[0-9a-fA-F]{6}$/)) cp.value = this.value;">
|
||||
oninput="var cp = this.closest('.space-y-6').querySelector('[data-starlark-color-picker={{ field_id }}]'); if(cp && this.value.match(/^#[0-9a-fA-F]{6}$/)) cp.value = this.value;">
|
||||
</div>
|
||||
{% if field_desc %}
|
||||
<p class="text-xs text-gray-400 mt-1">{{ field_desc }}</p>
|
||||
@@ -375,8 +375,15 @@ function toggleStarlarkApp(appId, enabled) {
|
||||
function saveStarlarkConfig(appId) {
|
||||
var config = {};
|
||||
|
||||
// Get container to scope queries (prevents conflicts if multiple modals open)
|
||||
var container = document.getElementById('plugin-config-starlark:' + appId);
|
||||
if (!container) {
|
||||
console.error('Container not found for appId:', appId);
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect standard inputs (text, number, select, datetime, color text companion)
|
||||
document.querySelectorAll('[data-starlark-config]').forEach(function(input) {
|
||||
container.querySelectorAll('[data-starlark-config]').forEach(function(input) {
|
||||
var key = input.getAttribute('data-starlark-config');
|
||||
var type = input.getAttribute('data-starlark-type');
|
||||
|
||||
@@ -390,7 +397,7 @@ function saveStarlarkConfig(appId) {
|
||||
});
|
||||
|
||||
// Collect location mini-form groups
|
||||
document.querySelectorAll('[data-starlark-location-group]').forEach(function(group) {
|
||||
container.querySelectorAll('[data-starlark-location-group]').forEach(function(group) {
|
||||
var fieldId = group.getAttribute('data-starlark-location-group');
|
||||
var loc = {};
|
||||
group.querySelectorAll('[data-starlark-location-field="' + fieldId + '"]').forEach(function(sub) {
|
||||
|
||||
Reference in New Issue
Block a user