fix: address remaining Codacy security findings

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 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-05-14 13:24:33 -04:00
parent 3aaf156962
commit 3bdd694b2e
5 changed files with 10 additions and 7 deletions

View File

@@ -111,7 +111,7 @@ def main():
# Ensure PYTHONPATH is set correctly if web_interface.py has relative imports to src # 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 # The WorkingDirectory in systemd service should handle this for web_interface.py
print(f"Launching web interface v3: {sys.executable} {WEB_INTERFACE_SCRIPT}") 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: except Exception as e:
print(f"Failed to exec web interface: {e}") print(f"Failed to exec web interface: {e}")
sys.exit(1) # Failed to start sys.exit(1) # Failed to start

View File

@@ -183,7 +183,7 @@ class DiskCache:
os.replace(tmp_path, cache_path) os.replace(tmp_path, cache_path)
# Set proper permissions: 660 (rw-rw----) for group-readable cache files # Set proper permissions: 660 (rw-rw----) for group-readable cache files
try: try:
os.chmod(cache_path, 0o660) os.chmod(cache_path, 0o660) # nosec B103 - intentional; web UI and service share a group
except OSError: except OSError:
pass # Non-critical if chmod fails pass # Non-critical if chmod fails
finally: finally:
@@ -201,7 +201,7 @@ class DiskCache:
os.fsync(cache_file.fileno()) os.fsync(cache_file.fileno())
# Set proper permissions: 660 (rw-rw----) for group-readable cache files # Set proper permissions: 660 (rw-rw----) for group-readable cache files
try: try:
os.chmod(cache_path, 0o660) os.chmod(cache_path, 0o660) # nosec B103 - intentional; web UI and service share a group
except OSError: except OSError:
pass # Non-critical if chmod fails pass # Non-critical if chmod fails
self.logger.debug("Wrote cache for %s directly (non-atomic)", key) 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) json.dump(data, tmp_file, indent=4, cls=DateTimeEncoder)
# Set proper permissions: 660 (rw-rw----) for group-readable cache files # Set proper permissions: 660 (rw-rw----) for group-readable cache files
try: try:
os.chmod(fallback_path, 0o660) os.chmod(fallback_path, 0o660) # nosec B103 - intentional; web UI and service share a group
except OSError: except OSError:
pass # Non-critical if chmod fails pass # Non-critical if chmod fails
self.logger.debug("Cache wrote to fallback location: %s", fallback_path) self.logger.debug("Cache wrote to fallback location: %s", fallback_path)

View File

@@ -267,7 +267,7 @@ class FontManager:
# Download font # Download font
logger.info(f"Downloading font from {url}") 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 # Handle zip files
if url.endswith('.zip'): if url.endswith('.zip'):

View File

@@ -937,7 +937,7 @@ class WiFiManager:
pass pass
try: try:
import urllib.request as _ureq 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") logger.debug("Internet connectivity confirmed via HTTP check")
return True return True
except OSError: except OSError:

View File

@@ -737,6 +737,9 @@ def check_health_monitor():
_threading.Thread(target=_run_startup_reconciliation, daemon=True).start() _threading.Thread(target=_run_startup_reconciliation, daemon=True).start()
if __name__ == '__main__': if __name__ == '__main__':
import os as _os
# threaded=True is Flask's default since 1.0 but stated explicitly so that # 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. # 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)