add security fixes, port hardening, and expanded QA coverage
Security fixes: - Replace debug=True with env-driven FLASK_DEBUG in app.py - Add _safe_path helper and path-traversal protection to all 6 file routes in file_manager.py - Add peer_name regex and input validation (public_key, name, endpoint_ip) in wireguard_manager.py - Stop returning private key from GET /api/wireguard/keys; return only public_key + has_private_key boolean - Fix is_local_request() XFF bypass by checking remote_addr only, ignoring X-Forwarded-For - Remove duplicate get_all_configs / get_config_summary methods from config_manager.py DevOps: - Bind 6 internal service ports to 127.0.0.1 in docker-compose.yml (radicale, webdav, api, webui, rainloop, filegator) - Move WebDAV credentials to env vars (WEBDAV_USER, WEBDAV_PASS) - Pin flask, flask-cors, requests, cryptography, docker to secure minimum versions in requirements.txt QA (560 tests, 0 failures): - tests/test_wireguard_endpoints.py: 18 new endpoint tests - tests/test_file_endpoints.py: 24 new endpoint tests incl. path traversal - tests/test_container_manager.py: expanded from 2 to 30 tests - tests/test_config_backup_restore_http.py: 25 new tests (new file) - tests/test_config_apply.py: 9 new tests (new file) Docs: - Rewrite README.md with accurate architecture, ports, env vars, security notes - Rewrite QUICKSTART.md with verified commands Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+30
-13
@@ -343,8 +343,16 @@ def _local_subnets():
|
||||
|
||||
|
||||
def is_local_request():
|
||||
# SECURITY: do NOT use X-Forwarded-For for auth. Caddy (and any reverse
|
||||
# proxy) sets XFF to the original client IP, but the TCP peer that reaches
|
||||
# this Flask process is always the proxy itself (an RFC-1918 Docker IP).
|
||||
# Trusting XFF would let any internet client claim a local IP via that
|
||||
# header. Only the direct TCP peer (request.remote_addr) is trustworthy:
|
||||
# all legitimate local traffic comes directly from the Docker network or
|
||||
# loopback, so remote_addr being local is a sufficient and necessary
|
||||
# condition. The XFF header is read for logging only, never for access
|
||||
# decisions.
|
||||
remote_addr = request.remote_addr
|
||||
forwarded_for = request.headers.get('X-Forwarded-For', '')
|
||||
|
||||
def _allowed(addr):
|
||||
if not addr:
|
||||
@@ -374,14 +382,7 @@ def is_local_request():
|
||||
pass
|
||||
return False
|
||||
|
||||
if _allowed(remote_addr):
|
||||
return True
|
||||
# Only trust the LAST X-Forwarded-For entry — that is what the reverse proxy appended.
|
||||
if forwarded_for:
|
||||
last_hop = forwarded_for.split(',')[-1].strip()
|
||||
if _allowed(last_hop):
|
||||
return True
|
||||
return False
|
||||
return _allowed(remote_addr)
|
||||
|
||||
@app.route('/health', methods=['GET'])
|
||||
def health_check():
|
||||
@@ -1416,10 +1417,13 @@ def test_network():
|
||||
# WireGuard API
|
||||
@app.route('/api/wireguard/keys', methods=['GET'])
|
||||
def get_wireguard_keys():
|
||||
"""Get WireGuard keys."""
|
||||
"""Get WireGuard keys (public key only; private key never leaves the server)."""
|
||||
try:
|
||||
result = wireguard_manager.get_keys()
|
||||
return jsonify(result)
|
||||
keys = wireguard_manager.get_keys()
|
||||
return jsonify({
|
||||
'public_key': keys.get('public_key', ''),
|
||||
'has_private_key': bool(keys.get('private_key')),
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting WireGuard keys: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
@@ -2149,6 +2153,8 @@ def create_folder():
|
||||
return jsonify({"error": "No data provided"}), 400
|
||||
result = file_manager.create_folder(data)
|
||||
return jsonify(result)
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating folder: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
@@ -2159,6 +2165,8 @@ def delete_folder(username, folder_path):
|
||||
try:
|
||||
result = file_manager.delete_folder(username, folder_path)
|
||||
return jsonify(result)
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting folder: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
@@ -2175,6 +2183,8 @@ def upload_file(username):
|
||||
|
||||
result = file_manager.upload_file(username, file, path)
|
||||
return jsonify(result)
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
except Exception as e:
|
||||
logger.error(f"Error uploading file: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
@@ -2185,6 +2195,8 @@ def download_file(username, file_path):
|
||||
try:
|
||||
result = file_manager.download_file(username, file_path)
|
||||
return jsonify(result)
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
except Exception as e:
|
||||
logger.error(f"Error downloading file: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
@@ -2195,6 +2207,8 @@ def delete_file(username, file_path):
|
||||
try:
|
||||
result = file_manager.delete_file(username, file_path)
|
||||
return jsonify(result)
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting file: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
@@ -2206,6 +2220,8 @@ def list_files(username):
|
||||
folder = request.args.get('folder', '')
|
||||
result = file_manager.list_files(username, folder)
|
||||
return jsonify(result)
|
||||
except ValueError as e:
|
||||
return jsonify({"error": str(e)}), 400
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing files: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
@@ -2915,4 +2931,5 @@ def remove_volume(name):
|
||||
return jsonify({'removed': success})
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host='0.0.0.0', port=3000, debug=True)
|
||||
debug = os.environ.get('FLASK_DEBUG', '0') == '1'
|
||||
app.run(host='0.0.0.0', port=3000, debug=debug)
|
||||
Reference in New Issue
Block a user