mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 16:58:06 +00:00
fix(dev-server): derive plugin dir from trusted directory listings
CodeQL's barrier-guard recognition doesn't see a startswith check inside an any() comprehension, so the normalize-and-prefix approach still flagged. Break the taint outright instead: after lookup, re-derive the directory by enumerating the search dirs (iterdir) and matching by path equality — the Path used for all downstream file access is built solely from trusted listings, never from request input. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FqzC1nzTWL4kaqgMaQZFam
This commit is contained in:
+22
-10
@@ -256,24 +256,36 @@ def _render_once(plugin_id, plugin_dir, manifest, config, mock_data, width, heig
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _trusted_plugin_dir(plugin_dir: Path) -> Optional[Path]:
|
||||||
|
"""Re-derive a plugin directory from the search dirs' own listings.
|
||||||
|
|
||||||
|
Path-injection barrier: the returned Path is constructed purely from
|
||||||
|
trusted directory enumeration (``iterdir``) — request-derived strings
|
||||||
|
never enter its construction — so a crafted plugin id can never make
|
||||||
|
downstream file access leave the plugin search dirs. Comparison is by
|
||||||
|
path equality, deliberately without symlink resolution (dev plugins
|
||||||
|
are commonly symlinked into plugins/).
|
||||||
|
"""
|
||||||
|
wanted = Path(os.path.normpath(str(plugin_dir)))
|
||||||
|
for search_dir in get_search_dirs():
|
||||||
|
if not search_dir.is_dir():
|
||||||
|
continue
|
||||||
|
for entry in search_dir.iterdir():
|
||||||
|
if entry.is_dir() and entry == wanted:
|
||||||
|
return entry
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _parse_render_request(data):
|
def _parse_render_request(data):
|
||||||
"""Shared /api/render* request prep. Returns (plugin_dir, manifest, config,
|
"""Shared /api/render* request prep. Returns (plugin_dir, manifest, config,
|
||||||
mock_data, skip_update) or raises ValueError with a client message."""
|
mock_data, skip_update) or raises ValueError with a client message."""
|
||||||
plugin_id = data['plugin_id']
|
plugin_id = data['plugin_id']
|
||||||
plugin_dir = find_plugin_dir(plugin_id)
|
plugin_dir = find_plugin_dir(plugin_id)
|
||||||
|
if plugin_dir:
|
||||||
|
plugin_dir = _trusted_plugin_dir(plugin_dir)
|
||||||
if not plugin_dir:
|
if not plugin_dir:
|
||||||
raise LookupError(f'Plugin not found: {plugin_id}')
|
raise LookupError(f'Plugin not found: {plugin_id}')
|
||||||
|
|
||||||
# Path-injection barrier: normalize (no symlink resolution — dev plugins
|
|
||||||
# are commonly symlinked into plugins/) and require the directory to sit
|
|
||||||
# inside one of the plugin search dirs before any file access.
|
|
||||||
normalized = os.path.normpath(str(plugin_dir))
|
|
||||||
allowed_roots = [os.path.normpath(str(d)) for d in get_search_dirs()]
|
|
||||||
if not any(normalized == root or normalized.startswith(root + os.sep)
|
|
||||||
for root in allowed_roots):
|
|
||||||
raise LookupError(f'Plugin not found: {plugin_id}')
|
|
||||||
plugin_dir = Path(normalized)
|
|
||||||
|
|
||||||
manifest_path = plugin_dir / 'manifest.json'
|
manifest_path = plugin_dir / 'manifest.json'
|
||||||
with open(manifest_path, 'r') as f:
|
with open(manifest_path, 'r') as f:
|
||||||
manifest = json.load(f)
|
manifest = json.load(f)
|
||||||
|
|||||||
Reference in New Issue
Block a user