mirror of
https://github.com/blw1138/cross-py-builder.git
synced 2026-09-07 21:41:09 -05:00
Harden build agent against path traversal and add job cleanup
- Validate job IDs on /download and /delete; reject invalid identifiers - Prevent zip-slip during /upload extraction via safe_extract - Basename uploaded filenames in /update to block path traversal - Route git-checkout jobs into pybuild-data so they can be downloaded/deleted - Clean up job dirs on extract, checkout, and build failures - Ignore Python bytecode and egg-info artifacts
This commit is contained in:
@@ -1 +1,4 @@
|
||||
known_hosts
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
@@ -9,6 +9,7 @@ import time
|
||||
|
||||
from flask import Flask, request, jsonify, send_file
|
||||
import os
|
||||
import re
|
||||
import zipfile
|
||||
import subprocess
|
||||
import glob
|
||||
@@ -46,26 +47,28 @@ def update_files():
|
||||
error_files = []
|
||||
for key in request.files:
|
||||
uploaded_file = request.files[key]
|
||||
if uploaded_file.filename:
|
||||
original_filename = uploaded_file.filename
|
||||
temp_save_path = os.path.join(LOCAL_DIR, f"{original_filename}.tmp")
|
||||
uploaded_file.save(temp_save_path)
|
||||
if os.path.getsize(temp_save_path):
|
||||
try:
|
||||
backup_path = os.path.join(LOCAL_DIR, original_filename + ".old")
|
||||
local_file_path = os.path.join(LOCAL_DIR, original_filename)
|
||||
os.rename(local_file_path, backup_path)
|
||||
shutil.move(temp_save_path, local_file_path)
|
||||
os.remove(backup_path)
|
||||
needs_install_requirements |= (requirements_path == local_file_path)
|
||||
updated_files.append(original_filename)
|
||||
except Exception as e:
|
||||
print(f"Exception updating file ({original_filename}): {e}")
|
||||
error_files.append(original_filename)
|
||||
else:
|
||||
print(f"Invalid size for {temp_save_path}!")
|
||||
original_filename = os.path.basename(uploaded_file.filename or "")
|
||||
if not original_filename:
|
||||
error_files.append(uploaded_file.filename)
|
||||
continue
|
||||
temp_save_path = os.path.join(LOCAL_DIR, f"{original_filename}.tmp")
|
||||
uploaded_file.save(temp_save_path)
|
||||
if os.path.getsize(temp_save_path):
|
||||
try:
|
||||
backup_path = os.path.join(LOCAL_DIR, original_filename + ".old")
|
||||
local_file_path = os.path.join(LOCAL_DIR, original_filename)
|
||||
os.rename(local_file_path, backup_path)
|
||||
shutil.move(temp_save_path, local_file_path)
|
||||
os.remove(backup_path)
|
||||
needs_install_requirements |= (requirements_path == local_file_path)
|
||||
updated_files.append(original_filename)
|
||||
except Exception as e:
|
||||
print(f"Exception updating file ({original_filename}): {e}")
|
||||
error_files.append(original_filename)
|
||||
os.remove(temp_save_path)
|
||||
else:
|
||||
print(f"Invalid size for {temp_save_path}!")
|
||||
error_files.append(original_filename)
|
||||
os.remove(temp_save_path)
|
||||
|
||||
if os.path.exists(requirements_path) and needs_install_requirements:
|
||||
print(f"\nInstalling Required Packages...")
|
||||
@@ -159,28 +162,47 @@ def status():
|
||||
def generate_job_id():
|
||||
return str(uuid.uuid4()).split('-')[-1]
|
||||
|
||||
JOB_ID_PATTERN = re.compile(r"[0-9a-f]{12}")
|
||||
|
||||
def is_valid_job_id(job_id):
|
||||
return bool(job_id) and bool(JOB_ID_PATTERN.fullmatch(job_id))
|
||||
|
||||
|
||||
@app.route("/checkout_git", methods=['POST'])
|
||||
def checkout_project():
|
||||
start_time = datetime.datetime.now()
|
||||
repo_url = request.json.get('repo_url')
|
||||
if not repo_url:
|
||||
if not request.is_json or not request.json.get('repo_url'):
|
||||
return jsonify({'error': 'Repository URL is required'}), 400
|
||||
|
||||
repo_url = request.json['repo_url']
|
||||
print(f"\n========== Checking Out Git Project ==========")
|
||||
|
||||
job_id = generate_job_id()
|
||||
repo_dir = os.path.join(TMP_DIR, job_id)
|
||||
build_root = os.path.join(TMP_DIR, BUILD_DIR)
|
||||
repo_dir = os.path.join(build_root, job_id)
|
||||
start_time = datetime.datetime.now()
|
||||
try:
|
||||
os.makedirs(build_root, exist_ok=True)
|
||||
system_status['status'] = "cloning_repo"
|
||||
subprocess.check_call(['git', 'clone', repo_url, repo_dir])
|
||||
system_status['status'] = "ready"
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"Error cloning repo: {e}")
|
||||
return install_and_build(repo_dir, job_id, start_time)
|
||||
except Exception as e:
|
||||
print(f"Error processing checkout: {e}")
|
||||
system_status['status'] = "ready"
|
||||
return jsonify({'error': 'Failed to clone repository'}), 500
|
||||
system_status['running_job'] = None
|
||||
shutil.rmtree(repo_dir, ignore_errors=True)
|
||||
if isinstance(e, subprocess.CalledProcessError):
|
||||
return jsonify({'error': 'Failed to clone repository'}), 500
|
||||
return jsonify({'error': f"Uncaught error processing checkout: {e}"}), 500
|
||||
|
||||
return install_and_build(repo_dir, job_id, start_time)
|
||||
def safe_extract(zip_ref, dest_dir):
|
||||
"""Extract a zipfile, rejecting members that would escape dest_dir."""
|
||||
dest_dir = os.path.realpath(dest_dir)
|
||||
for member in zip_ref.infolist():
|
||||
member_path = os.path.realpath(os.path.join(dest_dir, member.filename))
|
||||
if os.path.commonpath([dest_dir, member_path]) != dest_dir:
|
||||
raise ValueError(f"Unsafe path in archive: {member.filename!r}")
|
||||
zip_ref.extract(member, dest_dir)
|
||||
|
||||
@app.route('/upload', methods=['POST'])
|
||||
def upload_project():
|
||||
@@ -204,12 +226,13 @@ def upload_project():
|
||||
# Extract ZIP
|
||||
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
|
||||
print(f"Extracting uploaded project zip...")
|
||||
zip_ref.extractall(working_dir)
|
||||
safe_extract(zip_ref, working_dir)
|
||||
|
||||
return install_and_build(working_dir, job_id, start_time)
|
||||
except Exception as e:
|
||||
print(f"Uncaught error processing job: {e}")
|
||||
system_status['status'] = "ready"
|
||||
shutil.rmtree(working_dir, ignore_errors=True)
|
||||
return jsonify({"error": f"Uncaught error processing job: {e}"}), 500
|
||||
|
||||
def install_and_build(project_path, job_id, start_time):
|
||||
@@ -238,7 +261,7 @@ def install_and_build(project_path, job_id, start_time):
|
||||
print(f"Error setting up virtual environment: {e}")
|
||||
system_status['status'] = "ready"
|
||||
system_status['running_job'] = None
|
||||
os.rmdir(project_path)
|
||||
shutil.rmtree(project_path, ignore_errors=True)
|
||||
return jsonify({"error": f"Error setting up virtual environment: {e}"}), 500
|
||||
|
||||
# Install requirements
|
||||
@@ -255,7 +278,7 @@ def install_and_build(project_path, job_id, start_time):
|
||||
print(f"Error installing requirements: {e}")
|
||||
system_status['status'] = "ready"
|
||||
system_status['running_job'] = None
|
||||
os.rmdir(project_path)
|
||||
shutil.rmtree(project_path, ignore_errors=True)
|
||||
return jsonify({"error": f"Error installing requirements: {e}"}), 500
|
||||
|
||||
results = {}
|
||||
@@ -291,10 +314,7 @@ def install_and_build(project_path, job_id, start_time):
|
||||
print(f"Error compiling project: {e}")
|
||||
system_status['status'] = "ready"
|
||||
system_status['running_job'] = None
|
||||
try:
|
||||
os.remove(project_path)
|
||||
except PermissionError:
|
||||
pass
|
||||
shutil.rmtree(project_path, ignore_errors=True)
|
||||
return jsonify({"error": f"Error compiling project: {e}"}), 500
|
||||
|
||||
dist_path = os.path.join(project_path, "dist")
|
||||
@@ -331,6 +351,9 @@ def system_os():
|
||||
def download_binaries(job_id):
|
||||
"""Handles downloading the compiled PyInstaller binaries for a given job."""
|
||||
try:
|
||||
if not is_valid_job_id(job_id):
|
||||
return jsonify({"error": f"Invalid job id: {job_id}"}), 404
|
||||
|
||||
# Locate the build directory
|
||||
job_path = os.path.join(TMP_DIR, BUILD_DIR, job_id)
|
||||
dist_path = os.path.join(job_path, "dist")
|
||||
@@ -359,6 +382,9 @@ def download_binaries(job_id):
|
||||
|
||||
@app.route('/delete/<job_id>', methods=['GET'])
|
||||
def delete_project(job_id):
|
||||
if not is_valid_job_id(job_id):
|
||||
return jsonify({"error": f"Invalid job id: {job_id}"}), 404
|
||||
|
||||
job_path = os.path.join(TMP_DIR, BUILD_DIR, job_id)
|
||||
|
||||
if not os.path.exists(job_path):
|
||||
|
||||
Reference in New Issue
Block a user