mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-05-25 21:43:32 +00:00
Resolve CodeQL security findings in backup API
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>
This commit is contained in:
@@ -7154,7 +7154,8 @@ def backup_validate():
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
if not ok:
|
if not ok:
|
||||||
return jsonify({'status': 'error', 'message': err_msg}), 400
|
logger.warning("Backup validation failed: %s", err_msg)
|
||||||
|
return jsonify({'status': 'error', 'message': 'Invalid or corrupted backup file'}), 400
|
||||||
return jsonify({'status': 'success', 'data': manifest})
|
return jsonify({'status': 'success', 'data': manifest})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error("backup_validate failed: %s", e, exc_info=True)
|
logger.error("backup_validate failed: %s", e, exc_info=True)
|
||||||
@@ -7224,22 +7225,30 @@ def backup_restore():
|
|||||||
@api_v3.route('/backup/download/<path:filename>', methods=['GET'])
|
@api_v3.route('/backup/download/<path:filename>', methods=['GET'])
|
||||||
def backup_download(filename):
|
def backup_download(filename):
|
||||||
"""Stream a backup ZIP to the browser."""
|
"""Stream a backup ZIP to the browser."""
|
||||||
from flask import send_file
|
from flask import send_from_directory
|
||||||
path = _safe_backup_path(filename)
|
if _safe_backup_path(filename) is None:
|
||||||
if path is None or not path.exists():
|
return jsonify({'status': 'error', 'message': 'Backup not found'}), 404
|
||||||
|
try:
|
||||||
|
# send_from_directory uses werkzeug safe_join internally — CodeQL-recognized sanitizer.
|
||||||
|
return send_from_directory(_BACKUP_EXPORT_DIR, filename, as_attachment=True)
|
||||||
|
except FileNotFoundError:
|
||||||
return jsonify({'status': 'error', 'message': 'Backup not found'}), 404
|
return jsonify({'status': 'error', 'message': 'Backup not found'}), 404
|
||||||
return send_file(path, as_attachment=True, download_name=path.name)
|
|
||||||
|
|
||||||
|
|
||||||
@api_v3.route('/backup/<path:filename>', methods=['DELETE'])
|
@api_v3.route('/backup/<path:filename>', methods=['DELETE'])
|
||||||
def backup_delete(filename):
|
def backup_delete(filename):
|
||||||
"""Delete a stored backup ZIP."""
|
"""Delete a stored backup ZIP."""
|
||||||
path = _safe_backup_path(filename)
|
safe = _safe_backup_path(filename)
|
||||||
if path is None or not path.exists():
|
if safe is None:
|
||||||
return jsonify({'status': 'error', 'message': 'Backup not found'}), 404
|
return jsonify({'status': 'error', 'message': 'Backup not found'}), 404
|
||||||
|
# Enumerate the export directory and match by name so the unlink target is
|
||||||
|
# a filesystem-derived path rather than one constructed from user input.
|
||||||
try:
|
try:
|
||||||
path.unlink()
|
for entry in _BACKUP_EXPORT_DIR.iterdir():
|
||||||
return jsonify({'status': 'success'})
|
if entry.is_file() and entry.name == safe.name:
|
||||||
|
entry.unlink()
|
||||||
|
return jsonify({'status': 'success'})
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
logger.error("backup_delete failed: %s", e, exc_info=True)
|
logger.error("backup_delete failed: %s", e, exc_info=True)
|
||||||
return jsonify({'status': 'error', 'message': 'An internal error occurred; see logs for details'}), 500
|
return jsonify({'status': 'error', 'message': 'An internal error occurred; see logs for details'}), 500
|
||||||
|
return jsonify({'status': 'error', 'message': 'Backup not found'}), 404
|
||||||
Reference in New Issue
Block a user