mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-05-15 10:03:31 +00:00
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:
@@ -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
|
||||||
|
|||||||
6
src/cache/disk_cache.py
vendored
6
src/cache/disk_cache.py
vendored
@@ -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)
|
||||||
|
|||||||
@@ -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'):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user