- plugin_loader.py: resolve plugin_dir with strict=True and validate
marker_path with relative_to() before any filesystem writes, giving
CodeQL the positive sanitization pattern it requires (py/path-injection)
- api_v3.py _safe_backup_path: replace substring negative checks with a
strict positive regex (^[a-zA-Z0-9][a-zA-Z0-9._-]{0,200}\.zip$) that
CodeQL recognises as sanitising the user-supplied filename
(py/path-injection)
- api_v3.py backup_validate: whitelist known-safe manifest fields before
returning JSON, preventing any exception strings captured inside
validate_backup() from reaching the HTTP response (py/stack-trace-exposure)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- py/stack-trace-exposure: Remove str(e) and traceback.format_exc() from
all HTTP responses across api_v3.py, pages_v3.py, and app.py; replace
with generic messages and logger.error(exc_info=True)
- py/reflective-xss: Escape partial_name via markupsafe.escape in the
load_partial 404 response
- py/path-injection: Add regex validation of plugin_id before filesystem
use in _load_plugin_config_partial
- py/incomplete-url-substring-sanitization: Replace 'github.com' in
substring checks with urlparse hostname comparison in store_manager.py
- py/clear-text-logging-sensitive-data: Remove football-scoreboard debug
prints and sensitive request-body prints from update endpoint
- js/bad-tag-filter: Replace script-only regex in BaseWidget.sanitizeValue
with DOM-based textContent stripping that removes all HTML
- js/incomplete-sanitization: Fix escapeAttr to properly encode &, ", ',
<, > using HTML entities instead of backslash escaping
- js/prototype-pollution-utility: Add __proto__/constructor/prototype
key guards to deepMerge function in plugins_manager.js
- app.py error handlers: Always return generic messages; remove debug-mode
branches that could expose tracebacks in production
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- get_system_version (alert #218): replaced str(e) with generic message;
exception still logged via logger.error(exc_info=True)
- execute_system_action (alert #216): removed str(e) and full
traceback.format_exc() from the HTTP response — the full stack trace
was being sent directly to clients; replaced with generic message and
proper logger.error call
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Path traversal (CWE-22):
- backup_download: switch from send_file(user-tainted-path) to
send_from_directory(_BACKUP_EXPORT_DIR, filename); Flask uses
werkzeug safe_join internally which CodeQL recognises as a sanitizer
- backup_delete: enumerate the export directory and match by name so
entry.unlink() operates on a filesystem-derived Path rather than one
constructed from user input; _safe_backup_path still guards first
Information exposure through exceptions (CWE-209):
- backup_validate: err_msg from validate_backup() can embed exception
strings containing temp-file paths; log the detail, return a generic
'Invalid or corrupted backup file' to the client
- Other backup endpoints: already fixed (str(e) -> generic message);
CodeQL alerts will clear on next scan
plugin_loader.py:185 (path traversal): false positive — requirements_file
is constructed from plugin_dir returned by find_plugin_directory() (a
filesystem scan), not from raw HTTP request input; no change needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The previous check used a string replacement that left 'error:' in the
remaining text, causing the condition to always evaluate false. Simplify
to a direct substring check: if 'uninstall-no-record-file' appears in pip
stderr the affected package is installed at the system level and we write
the marker, suppressing the repeated warning on every restart.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a plugin's requirements.txt includes a package installed via the
system package manager (dnf/apt), pip fails with 'uninstall-no-record-file'
because it can't replace the system-tracked copy. The package is present
and functional, but the missing marker caused the install to be retried
on every service restart.
Detect this specific error pattern: if the only pip failure is
uninstall-no-record-file, write the .dependencies_installed marker and
log a warning instead of returning False, suppressing the repeated warning.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backup endpoints: replace raw str(e) in user-facing responses with a
generic message; full exception still logged via exc_info=True
- hardware/status: change ok:null to ok:false for PermissionError and
json.JSONDecodeError so the UI's hw.ok===false check triggers correctly
- base.html: dispatch htmx:ready from the fallback load path so any
deferred listeners fire on CDN-fallback loads too
- loadTabContent: also listen for htmx-load-failed so overview/wifi/plugins
fall back to direct fetch when HTMX is completely unavailable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 5-second setTimeout fallbacks for plugins and overview were firing
before the htmx:ready event arrived, logging spurious warnings. Each
timer now self-cancels via htmx:ready so the fallback only triggers
when HTMX genuinely fails to load.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add all backup API routes to api_v3.py: preview, list, export,
validate, restore (with plugin reinstall), download, delete
- Fix PermissionError on /hardware/status: return graceful 200 instead
of 500 when the status file is owned by a different user; also fix
root cause by writing the file world-readable (0o644) in display_manager
- Fix HTMX race: dispatch htmx:ready window event from HTMX onload
callback; loadTabContent now waits for that event instead of
immediately falling back to direct fetch (eliminating the
"HTMX not available" console warning on initial load)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 500ms fallback setTimeout was calling attachInstallButtonHandler()
unconditionally even when the plugins partial wasn't in the DOM, causing
a spurious console.warn on every page load. Add the same element-existence
check already present on the htmx:afterSettle listener.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In the catch block of all 11 hx-on:htmx:after-request handlers, check
xhr.status >= 400 and downgrade s to 'error' so a failed action that
returns an HTML error page (or other non-JSON body) surfaces as an error
toast instead of the optimistic 'success'/'info' default.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace silent catch(e){} in all 11 hx-on:htmx:after-request handlers with a
pattern that sets default message/status before the try block and calls
showNotification(m,s) unconditionally after it, so a fallback toast is shown
whenever xhr is absent or responseText is not valid JSON.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- base.html: add htmx:afterSettle listener to set data-loaded on tab
containers after HTMX swaps their content, preventing the overview
partial from being re-fetched (and handlers lost) on every tab switch
- base.html: call htmx.process() in loadOverviewDirect/loadPluginsDirect
fallbacks so buttons get HTMX handlers even if HTMX finished its
initial body scan before the fallback fetch completed
- overview.html + index.html (11 buttons): replace event.detail.xhr.responseJSON
(undefined in HTMX 1.9.x) with JSON.parse(event.detail.xhr.responseText)
so quick action toast notifications actually fire
- plugins_manager.js: add guarded htmx:afterSettle listener that only calls
attachInstallButtonHandler when #install-plugin-from-url is in the DOM,
eliminating the spurious console warning on non-plugin tab loads
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>