安全与质量修复: P0-4/P0-5 + P1-6~P1-13 (8项)
P0-4: install.py _write_env() 值加引号转义,防止含#$\等字符的密码破坏.env格式 P0-5: install.py _build_redis_url() URL编码redis密码,防止@:等字符破坏URL解析 P1-6: auth_service.py _decode_access_token() 验证exp/sub声明存在,拒绝畸形JWT P1-7: websocket.py + webssh.py WebSocket JWT验证增加updated_at检查,密码修改后令牌失效 P1-8: auth.py 无refresh_token的logout返回更明确的提示信息 P1-9: install.py create_admin增加_is_installed()检查,防止.env存在后重复创建 P1-10: servers.py server_stats() 改用SQL聚合查询,避免加载全部服务器对象 P1-11: heartbeat_flush.py 移除get_redis()永不返回None的死代码 P1-13: sync_engine_v2.py completed/failed计数器加asyncio.Lock防止并发竞态 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+27
-14
@@ -12,7 +12,7 @@ import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
@@ -85,7 +85,6 @@ def _escape_php_string(s: str) -> str:
|
||||
|
||||
|
||||
def _make_db_url(req: InitDbRequest | CreateAdminRequest) -> str:
|
||||
from urllib.parse import quote_plus
|
||||
return (
|
||||
f"mysql+aiomysql://{req.db_user}:{quote_plus(req.db_pass)}"
|
||||
f"@{req.db_host}:{req.db_port}/{req.db_name}"
|
||||
@@ -95,17 +94,29 @@ def _make_db_url(req: InitDbRequest | CreateAdminRequest) -> str:
|
||||
def _build_redis_url(req: InitDbRequest) -> str:
|
||||
url = "redis://"
|
||||
if req.redis_pass:
|
||||
url += f"{req.redis_pass}@"
|
||||
url += f"{quote_plus(req.redis_pass)}@"
|
||||
url += f"{req.redis_host}:{req.redis_port}/{req.redis_db}"
|
||||
return url
|
||||
|
||||
|
||||
def _escape_env_value(value: str) -> str:
|
||||
"""Escape a value for safe inclusion in a .env file.
|
||||
|
||||
.env files are essentially shell source files — unquoted values break
|
||||
on spaces, ``#``, ``$``, newlines, backslashes, etc. The safest
|
||||
approach is to double-quote the value and escape any internal
|
||||
double-quotes and backslashes (Docker/python-dotenv convention).
|
||||
"""
|
||||
escaped = value.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n")
|
||||
return f'"{escaped}"'
|
||||
|
||||
|
||||
def _write_env(req: InitDbRequest, secret_key: str, api_key: str, encryption_key: str,
|
||||
pool_size: int, max_overflow: int, redis_url: str,
|
||||
site_url: str) -> None:
|
||||
install_dir = str(ROOT_DIR)
|
||||
db_url = (
|
||||
f"mysql+aiomysql://{req.db_user}:{req.db_pass}"
|
||||
f"mysql+aiomysql://{req.db_user}:{quote_plus(req.db_pass)}"
|
||||
f"@{req.db_host}:{req.db_port}/{req.db_name}"
|
||||
)
|
||||
|
||||
@@ -113,35 +124,35 @@ def _write_env(req: InitDbRequest, secret_key: str, api_key: str, encryption_key
|
||||
"# Nexus .env — Auto-generated by installer",
|
||||
f"# {datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S')} UTC",
|
||||
"",
|
||||
"NEXUS_SYSTEM_NAME=Nexus",
|
||||
"NEXUS_SYSTEM_TITLE=Nexus — 服务器运维管理平台",
|
||||
f"NEXUS_SYSTEM_NAME={_escape_env_value('Nexus')}",
|
||||
f"NEXUS_SYSTEM_TITLE={_escape_env_value('Nexus — 服务器运维管理平台')}",
|
||||
"",
|
||||
"NEXUS_HOST=0.0.0.0",
|
||||
f"NEXUS_PORT={req.api_port}",
|
||||
"",
|
||||
"# Database (MySQL 8.4 + aiomysql async driver)",
|
||||
f"NEXUS_DATABASE_URL={db_url}",
|
||||
f"NEXUS_DATABASE_URL={_escape_env_value(db_url)}",
|
||||
f"NEXUS_DB_POOL_SIZE={pool_size}",
|
||||
f"NEXUS_DB_MAX_OVERFLOW={max_overflow}",
|
||||
"",
|
||||
"# Security — PRODUCTION KEYS (immutable after install)",
|
||||
f"NEXUS_SECRET_KEY={secret_key}",
|
||||
f"NEXUS_API_KEY={api_key}",
|
||||
f"NEXUS_ENCRYPTION_KEY={encryption_key}",
|
||||
f"NEXUS_SECRET_KEY={_escape_env_value(secret_key)}",
|
||||
f"NEXUS_API_KEY={_escape_env_value(api_key)}",
|
||||
f"NEXUS_ENCRYPTION_KEY={_escape_env_value(encryption_key)}",
|
||||
"",
|
||||
"# Redis",
|
||||
f"NEXUS_REDIS_URL={redis_url}",
|
||||
f"NEXUS_REDIS_URL={_escape_env_value(redis_url)}",
|
||||
"",
|
||||
"# Deployment",
|
||||
f"NEXUS_DEPLOY_PATH={install_dir}",
|
||||
f"NEXUS_CORS_ORIGINS={site_url},http://localhost:{req.api_port}",
|
||||
f"NEXUS_DEPLOY_PATH={_escape_env_value(install_dir)}",
|
||||
f"NEXUS_CORS_ORIGINS={_escape_env_value(f'{site_url},http://localhost:{req.api_port}')}",
|
||||
"",
|
||||
"# SSH",
|
||||
"NEXUS_SSH_STRICT_HOST_CHECKING=false",
|
||||
"",
|
||||
"# Health Check",
|
||||
"NEXUS_HEALTH_CHECK_INTERVAL=60",
|
||||
f"NEXUS_API_BASE_URL={site_url}",
|
||||
f"NEXUS_API_BASE_URL={_escape_env_value(site_url)}",
|
||||
"",
|
||||
"# Alert Thresholds",
|
||||
"NEXUS_CPU_ALERT_THRESHOLD=80",
|
||||
@@ -508,6 +519,8 @@ async def init_db(req: InitDbRequest):
|
||||
@router.post("/create-admin")
|
||||
async def create_admin(req: CreateAdminRequest):
|
||||
"""Step 4: Create admin account and set brand name."""
|
||||
if _is_installed():
|
||||
raise HTTPException(400, "系统已安装,不可重复操作")
|
||||
if _is_locked():
|
||||
raise HTTPException(400, "系统已锁定")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user