From 3bdd694b2ee004b3ef2aa5256644f57ab5e388e2 Mon Sep 17 00:00:00 2001 From: Chuck Date: Thu, 14 May 2026 13:24:33 -0400 Subject: [PATCH] fix: address remaining Codacy security findings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flask debug=True (real fix): - web_interface/app.py: debug=True in __main__ block exposes the Werkzeug interactive debugger (arbitrary code execution). Changed to os.environ.get('FLASK_DEBUG', '0') == '1' — off by default, opt-in via environment variable for local development. nosec annotations (accepted risk with documented rationale): - disk_cache.py: os.chmod(0o660) is intentional — web UI and LED matrix service share a group, 660 gives group write while denying world access (B103 + Semgrep insecure-file-permissions suppressed in Codacy) - wifi_manager.py: urlopen to hardcoded connectivity-check.ubuntu.com URL (B310 — no user input involved) - font_manager.py: urlretrieve URL comes from user's own config file on their local device (B310) - start_web_conditionally.py: os.execvp with both sys.executable and a fixed PROJECT_DIR-relative constant (B606) Confirmed false positives suppressed via Codacy API (15 issues): - SSRF (3x): client-side JS fetch — SSRF is server-side; browser fetch is CORS-restricted to same origin - B105 (3x): test fixtures use dummy secrets by design; store_manager checks for the placeholder string, it is not itself a secret - PMD numeric literal (2x): 10000000 is within Number.MAX_SAFE_INTEGER - Prototype pollution (1x): read-only schema traversal, no writes - no-unsanitized_method (1x): dynamic import() is CORS-restricted - detect-unsafe-regex (1x): operates on server-controlled config values - plugin-repos B103 (1x): vendor code chmod on executable - Semgrep insecure-file-permissions (3x): same disk_cache 0o660 as above Co-Authored-By: Claude Sonnet 4.6 --- scripts/utils/start_web_conditionally.py | 2 +- src/cache/disk_cache.py | 6 +++--- src/font_manager.py | 2 +- src/wifi_manager.py | 2 +- web_interface/app.py | 5 ++++- 5 files changed, 10 insertions(+), 7 deletions(-) diff --git a/scripts/utils/start_web_conditionally.py b/scripts/utils/start_web_conditionally.py index b8f6e351..b3c3c99d 100644 --- a/scripts/utils/start_web_conditionally.py +++ b/scripts/utils/start_web_conditionally.py @@ -111,7 +111,7 @@ def main(): # Ensure PYTHONPATH is set correctly if web_interface.py has relative imports to src # The WorkingDirectory in systemd service should handle this for web_interface.py print(f"Launching web interface v3: {sys.executable} {WEB_INTERFACE_SCRIPT}") - os.execvp(sys.executable, [sys.executable, WEB_INTERFACE_SCRIPT]) + os.execvp(sys.executable, [sys.executable, WEB_INTERFACE_SCRIPT]) # nosec B606 - both args are fixed constants except Exception as e: print(f"Failed to exec web interface: {e}") sys.exit(1) # Failed to start diff --git a/src/cache/disk_cache.py b/src/cache/disk_cache.py index 8a9d6da2..13992dfc 100644 --- a/src/cache/disk_cache.py +++ b/src/cache/disk_cache.py @@ -183,7 +183,7 @@ class DiskCache: os.replace(tmp_path, cache_path) # Set proper permissions: 660 (rw-rw----) for group-readable cache files try: - os.chmod(cache_path, 0o660) + os.chmod(cache_path, 0o660) # nosec B103 - intentional; web UI and service share a group except OSError: pass # Non-critical if chmod fails finally: @@ -201,7 +201,7 @@ class DiskCache: os.fsync(cache_file.fileno()) # Set proper permissions: 660 (rw-rw----) for group-readable cache files try: - os.chmod(cache_path, 0o660) + os.chmod(cache_path, 0o660) # nosec B103 - intentional; web UI and service share a group except OSError: pass # Non-critical if chmod fails self.logger.debug("Wrote cache for %s directly (non-atomic)", key) @@ -227,7 +227,7 @@ class DiskCache: json.dump(data, tmp_file, indent=4, cls=DateTimeEncoder) # Set proper permissions: 660 (rw-rw----) for group-readable cache files try: - os.chmod(fallback_path, 0o660) + os.chmod(fallback_path, 0o660) # nosec B103 - intentional; web UI and service share a group except OSError: pass # Non-critical if chmod fails self.logger.debug("Cache wrote to fallback location: %s", fallback_path) diff --git a/src/font_manager.py b/src/font_manager.py index 6372fd6c..76dc7ec5 100644 --- a/src/font_manager.py +++ b/src/font_manager.py @@ -267,7 +267,7 @@ class FontManager: # Download font logger.info(f"Downloading font from {url}") - urllib.request.urlretrieve(url, cache_path) + urllib.request.urlretrieve(url, cache_path) # nosec B310 - URL from user's own config file on local device # Handle zip files if url.endswith('.zip'): diff --git a/src/wifi_manager.py b/src/wifi_manager.py index 21b87035..09e1f3a5 100644 --- a/src/wifi_manager.py +++ b/src/wifi_manager.py @@ -937,7 +937,7 @@ class WiFiManager: pass try: import urllib.request as _ureq - _ureq.urlopen("http://connectivity-check.ubuntu.com/", timeout=timeout) + _ureq.urlopen("http://connectivity-check.ubuntu.com/", timeout=timeout) # nosec B310 - hardcoded URL, no user input logger.debug("Internet connectivity confirmed via HTTP check") return True except OSError: diff --git a/web_interface/app.py b/web_interface/app.py index 4b02232d..393fef17 100644 --- a/web_interface/app.py +++ b/web_interface/app.py @@ -737,6 +737,9 @@ def check_health_monitor(): _threading.Thread(target=_run_startup_reconciliation, daemon=True).start() if __name__ == '__main__': + import os as _os # threaded=True is Flask's default since 1.0 but stated explicitly so that # long-lived /api/v3/stream/* SSE connections don't starve other requests. - app.run(host='0.0.0.0', port=5000, debug=True, threaded=True) + # Debug mode is off by default; opt in with FLASK_DEBUG=1 in the environment. + _debug = _os.environ.get('FLASK_DEBUG', '0') == '1' + app.run(host='0.0.0.0', port=5000, debug=_debug, threaded=True)