fix(security): protect admin credentials and plugin downloads
Build and publish Docker image / build-and-publish (push) Successful in 19s

This commit is contained in:
wfalkowski committed 2026-10-20 17:17:00 +00:00
1 parent 84381ddc23
commit 91d7c78392
2 files changed
+42 -9

No files matched your search

+6 -1
View File
@@ -196,6 +196,11 @@ def admin_get_user_servers(user_id):
return jsonify({'success': False, 'error': 'Admin access required'}), 403 return jsonify({'success': False, 'error': 'Admin access required'}), 403
servers = get_user_servers(user_id) servers = get_user_servers(user_id)
# Admin server management does not need connection credentials.
servers = [
{key: value for key, value in server.items() if key != 'sftp_password'}
for server in servers
]
return jsonify({'success': True, 'servers': servers}), 200 return jsonify({'success': True, 'servers': servers}), 200
except Exception as e: except Exception as e:
@@ -331,7 +336,7 @@ def admin_get_all_servers():
cursor.execute(''' cursor.execute('''
SELECT s.id, s.name, s.cpu, s.ram, s.storage, s.image, s.type, s.version, s.loader, s.java_version, SELECT s.id, s.name, s.cpu, s.ram, s.storage, s.image, s.type, s.version, s.loader, s.java_version,
s.status, s.ports, s.created_at, u.username as owner, u.id as owner_id, s.status, s.ports, s.created_at, u.username as owner, u.id as owner_id,
s.sftp_username, s.sftp_password, s.sftp_port s.sftp_username, s.sftp_port
FROM servers s FROM servers s
JOIN users u ON s.owner_id = u.id JOIN users u ON s.owner_id = u.id
ORDER BY u.username, s.name ORDER BY u.username, s.name
+36 -8
View File
@@ -4,8 +4,10 @@
from datetime import datetime from datetime import datetime
from flask import Blueprint, jsonify, request from flask import Blueprint, jsonify, request
import json import json
from urllib.parse import urlsplit
from kubernetes_service import k8s_manager from kubernetes_service import k8s_manager
import requests import requests
from werkzeug.utils import secure_filename
from runtime_config import ( from runtime_config import (
MODRINTH_API_BASE, MODRINTH_API_BASE,
logger, logger,
@@ -317,6 +319,21 @@ def download_plugin_to_server():
if not server_id or not plugin_url: if not server_id or not plugin_url:
return jsonify({'error': 'Server ID and plugin URL are required'}), 400 return jsonify({'error': 'Server ID and plugin URL are required'}), 400
try:
parsed_plugin_url = urlsplit(plugin_url)
plugin_port = parsed_plugin_url.port
except (TypeError, ValueError):
return jsonify({'error': 'Plugin URL is invalid'}), 400
if (parsed_plugin_url.scheme != 'https'
or parsed_plugin_url.hostname != 'cdn.modrinth.com'
or parsed_plugin_url.username or parsed_plugin_url.password
or plugin_port not in (None, 443)):
return jsonify({'error': 'Plugin files must come from the Modrinth CDN'}), 400
safe_file_name = secure_filename(str(file_name or 'plugin.jar'))
if not safe_file_name.lower().endswith('.jar'):
return jsonify({'error': 'Plugin file name must end in .jar'}), 400
# Verify server belongs to user # Verify server belongs to user
user_servers = get_user_servers(user['id']) user_servers = get_user_servers(user['id'])
server = next((s for s in user_servers if s['id'] == server_id), None) server = next((s for s in user_servers if s['id'] == server_id), None)
@@ -330,26 +347,37 @@ def download_plugin_to_server():
# Download plugin file # Download plugin file
logger.info(f"Downloading plugin from {plugin_url} for server {server_id}") logger.info(f"Downloading plugin from {plugin_url} for server {server_id}")
plugin_response = requests.get(plugin_url, stream=True, timeout=30) plugin_response = requests.get(
plugin_url, stream=True, timeout=30, allow_redirects=False
)
if plugin_response.status_code != 200: if plugin_response.status_code != 200:
plugin_response.close()
return jsonify({'error': f'Failed to download plugin: HTTP {plugin_response.status_code}'}), 500 return jsonify({'error': f'Failed to download plugin: HTTP {plugin_response.status_code}'}), 500
# Determine plugins directory based on server type # Determine plugins directory based on server type
# For Minecraft servers, plugins go in /data/plugins directory # For Minecraft servers, plugins go in /data/plugins directory
plugins_dir = '/data/plugins' plugins_dir = '/data/plugins'
file_path = f'{plugins_dir}/{file_name}' file_path = f'{plugins_dir}/{safe_file_name}'
# Save plugin to temporary file first # Save plugin to temporary file first
import tempfile import tempfile
import os import os
with tempfile.NamedTemporaryFile(delete=False, suffix='.jar') as temp_file: temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.jar')
temp_file_path = temp_file.name temp_file_path = temp_file.name
for chunk in plugin_response.iter_content(chunk_size=8192):
if chunk:
temp_file.write(chunk)
try: try:
with temp_file:
try:
total_bytes = 0
for chunk in plugin_response.iter_content(chunk_size=8192):
if chunk:
total_bytes += len(chunk)
if total_bytes > 100 * 1024 * 1024:
raise ValueError('Plugin file exceeds the 100 MB limit')
temp_file.write(chunk)
finally:
plugin_response.close()
# Upload to server # Upload to server
namespace = f"user-{user['username']}".lower() namespace = f"user-{user['username']}".lower()
pods = k8s_manager.get_pods_for_server(namespace, server_id) pods = k8s_manager.get_pods_for_server(namespace, server_id)