mirror of
https://github.com/dnslin/aria2bot.git
synced 2026-01-12 04:22:21 +08:00
fix: 修复安全漏洞和代码质量问题
安全修复: - 修复路径遍历检查,使用 Path.relative_to() 替代字符串前缀检查 - 修复 Zip Slip 漏洞,添加符号链接检查和路径验证 - 隐藏 RPC 密钥显示,防止敏感信息泄露 - 设置配置文件权限为 0o600 Bug 修复: - 修复 HTTP 状态码检查(resp.status → resp.code) - 修复 OneDrive 认证 flow 参数类型 - 修复 RPC 请求缺少状态码验证 - 修复配置文件渲染会替换注释行的问题 代码改进: - 添加 subprocess 超时处理,防止进程挂起 - 修复异步代码问题(get_event_loop → get_running_loop) - 使用 asyncio.to_thread 避免阻塞事件循环 - 添加 httpx 超时和状态码异常处理 - 移除无用的 ONEDRIVE_CLIENT_SECRET 配置 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -197,6 +197,10 @@ class Aria2Installer:
|
||||
new_lines: list[str] = []
|
||||
for line in content.splitlines():
|
||||
stripped = line.lstrip()
|
||||
# 跳过注释行,不进行替换
|
||||
if stripped.startswith("#"):
|
||||
new_lines.append(line)
|
||||
continue
|
||||
replaced = False
|
||||
for key, value in replacements.items():
|
||||
if stripped.startswith(key):
|
||||
@@ -209,7 +213,10 @@ class Aria2Installer:
|
||||
|
||||
try:
|
||||
ARIA2_CONF.write_text("\n".join(new_lines) + "\n", encoding="utf-8")
|
||||
# 设置配置文件权限为仅所有者可读写(包含敏感的 RPC 密钥)
|
||||
ARIA2_CONF.chmod(0o600)
|
||||
ARIA2_SESSION.touch(exist_ok=True)
|
||||
ARIA2_SESSION.chmod(0o600)
|
||||
self.config.download_dir.mkdir(parents=True, exist_ok=True)
|
||||
ARIA2_LOG.touch(exist_ok=True)
|
||||
logger.info(f"配置文件已保存: {ARIA2_CONF}")
|
||||
@@ -276,8 +283,10 @@ class Aria2Installer:
|
||||
req = request.Request(url, headers={"User-Agent": "aria2-installer"})
|
||||
try:
|
||||
with request.urlopen(req, timeout=30) as resp:
|
||||
if getattr(resp, "status", 200) >= 400:
|
||||
raise DownloadError(f"HTTP {resp.status} for {url}")
|
||||
# 检查 HTTP 状态码(urllib 使用 code 属性)
|
||||
status_code = getattr(resp, "code", 200)
|
||||
if status_code >= 400:
|
||||
raise DownloadError(f"HTTP {status_code} for {url}")
|
||||
return resp.read()
|
||||
except (error.HTTPError, error.URLError) as exc:
|
||||
raise DownloadError(f"Network error for {url}: {exc}") from exc
|
||||
@@ -292,8 +301,16 @@ class Aria2Installer:
|
||||
with tarfile.open(archive_path, "r:gz") as tar:
|
||||
# 安全检查:验证所有成员路径,防止 Zip Slip 攻击
|
||||
for member in tar.getmembers():
|
||||
# 检查符号链接
|
||||
if member.issym() or member.islnk():
|
||||
raise DownloadError(f"不安全的 tar 成员(符号链接): {member.name}")
|
||||
# 检查路径遍历
|
||||
if member.name.startswith('/') or '..' in member.name:
|
||||
raise DownloadError(f"不安全的 tar 成员: {member.name}")
|
||||
# 验证解压后的路径
|
||||
member_path = (extract_dir / member.name).resolve()
|
||||
if not str(member_path).startswith(str(extract_dir.resolve())):
|
||||
raise DownloadError(f"不安全的 tar 成员(路径遍历): {member.name}")
|
||||
tar.extractall(extract_dir)
|
||||
for candidate in extract_dir.rglob("aria2c"):
|
||||
if candidate.is_file():
|
||||
|
||||
@@ -86,9 +86,14 @@ class Aria2RpcClient:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10) as client:
|
||||
resp = await client.post(self.url, json=payload)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
except httpx.ConnectError:
|
||||
raise RpcError("aria2 服务可能未运行,请先使用 /start 命令启动服务") from None
|
||||
except httpx.TimeoutException:
|
||||
raise RpcError("RPC 请求超时,aria2 服务响应缓慢") from None
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise RpcError(f"RPC 请求失败,HTTP 状态码: {e.response.status_code}") from e
|
||||
except httpx.RequestError as e:
|
||||
raise RpcError(f"RPC 请求失败: {e}") from e
|
||||
except json.JSONDecodeError as e:
|
||||
@@ -184,7 +189,9 @@ class Aria2RpcClient:
|
||||
# 安全检查:验证路径在下载目录内,防止路径遍历攻击
|
||||
from src.core.constants import DOWNLOAD_DIR
|
||||
download_dir = DOWNLOAD_DIR.resolve()
|
||||
if not str(file_path).startswith(str(download_dir) + "/"):
|
||||
try:
|
||||
file_path.relative_to(download_dir)
|
||||
except ValueError:
|
||||
logger.error(f"路径遍历尝试被阻止: {file_path}")
|
||||
return False
|
||||
if file_path.exists():
|
||||
|
||||
@@ -48,9 +48,12 @@ class Aria2ServiceManager:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
timeout=30,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
raise ServiceError("systemctl command not found") from exc
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise ServiceError(f"systemctl 命令超时: {args}") from exc
|
||||
except subprocess.CalledProcessError as exc:
|
||||
output = exc.stderr.strip() or exc.stdout.strip() or str(exc)
|
||||
raise ServiceError(output) from exc
|
||||
@@ -102,15 +105,19 @@ class Aria2ServiceManager:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=10,
|
||||
)
|
||||
enabled_proc = subprocess.run(
|
||||
["systemctl", "--user", "is-enabled", "aria2"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=10,
|
||||
)
|
||||
except FileNotFoundError as exc:
|
||||
raise ServiceError("systemctl command not found") from exc
|
||||
except subprocess.TimeoutExpired as exc:
|
||||
raise ServiceError("获取服务状态超时") from exc
|
||||
|
||||
running = active_proc.returncode == 0
|
||||
enabled = enabled_proc.returncode == 0
|
||||
@@ -130,8 +137,9 @@ class Aria2ServiceManager:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=5,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
result = None
|
||||
|
||||
if result and result.returncode == 0:
|
||||
@@ -146,8 +154,9 @@ class Aria2ServiceManager:
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
timeout=5,
|
||||
)
|
||||
except FileNotFoundError:
|
||||
except (FileNotFoundError, subprocess.TimeoutExpired):
|
||||
return None
|
||||
|
||||
for line in ps_result.stdout.splitlines():
|
||||
|
||||
Reference in New Issue
Block a user