威胁模型:拥有普通账号的已登录用户攻击服务器。完整审计报告见
_experiments/security_audit_2026-09-02/(不入库),含实测复现记录。
严重/高危修复:
- 彻底移除文件夹打包下载(/api/download/folder 端点删除,gui/api_v1
目录下载分支改 410):文件型符号链接会被宿主进程解析,实测可读宿主
任意文件(含 settings.json 全部 LLM 密钥),删除比补校验更彻底
- XFF 伪造防护:get_client_ip 仅信任 ASTRION_TRUSTED_PROXIES
(默认空=不信 XFF);登录新增账号维度锁定(5 次失败锁 300s);
注册邀请码加爆破锁定;限流桶/失败表加 2 万键上限+GC 回收
- auth_debug.log 接入大小轮转;/api/client_debug_log 加限流+长度截断
- /host-login 增加 LINUX_SAFETY 检查且仅回环地址可用
容器加固:
- 默认 cpus=1 / memory=1g,新增 pids-limit=512、memory-swap、
no-new-privileges
- 新增每用户容器配额 MAX_ACTIVE_CONTAINERS_PER_USER(默认 3),
防止单用户占满全局容器池
api_v1:
- prompts/personalizations 的 name 加白名单校验(^[A-Za-z0-9_-]{1,64}$),
修复路径穿越写入;对话元数据中的引用名同步加白名单
- workspaces/conversations/messages/upload 四端点加用户维度限流
- 消息体加 MAX_MESSAGE_CHARS 上限(默认 200000,/api/tasks 同步)
- _path_within 加分隔符边界(修复 startswith 前缀碰撞)
其他:
- SVG 预览强制 application/octet-stream(修存储型 XSS 漏网)
- monitor_snapshot 缓存键加 username 维度(修跨用户快照读取)
- ensure/delete_workspace 加 workspace_id 白名单+父目录二次核验
- delete_folder 拒绝删除工作区根(crud_mixin 与容器代理同步修)
- GuiFileManager 死代码加名称校验防复活
- admin_dashboard 静态壳非 admin 访问一律 404
- /api/app/apk/latest 加登录校验+限流(原未认证可拉 133MB)
有意未修(见报告 7.3 遗留清单):容器 egress 过滤(部署层)、
--cap-drop ALL(怕破坏容器内工作流,已先上 no-new-privileges)、
API 用户/子智能体 LLM 按 token 计费(待产品决策)、str(exc) 收口、
WebSocket 限流、network_permission=restricted 容器语义。
验证:全部文件 py_compile 通过;冒烟测试 6/6 通过;关键新逻辑
(名称校验/路径边界/容器代理删除防护/限流回收)已单测级验证;
端点级行为待服务重启后实测复核(清单见报告 7.5)。
Co-authored-by: Astrion powered by Kimi-K3 <astrion-agent@users.noreply.github.com>
99 lines
3.6 KiB
Python
99 lines
3.6 KiB
Python
from __future__ import annotations
|
||
from server.status import status_bp
|
||
from server.status.docker import (
|
||
_parse_android_version_from_gradle,
|
||
_resolve_android_apk_path,
|
||
_load_app_changelog,
|
||
)
|
||
import time
|
||
import re
|
||
import os
|
||
import json
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
import plistlib
|
||
from pathlib import Path
|
||
from flask import Blueprint, jsonify, request, send_file, session
|
||
|
||
from server.auth_helpers import api_login_required, resolve_admin_policy
|
||
from server.context import with_terminal, attach_user_broadcast
|
||
from server.state import (
|
||
PROJECT_STORAGE_CACHE,
|
||
PROJECT_STORAGE_CACHE_TTL_SECONDS,
|
||
PROJECT_MAX_STORAGE_MB,
|
||
container_manager,
|
||
user_manager,
|
||
)
|
||
from config import AGENT_VERSION, TERMINAL_SANDBOX_MODE
|
||
from modules.host_workspace_manager import (
|
||
create_host_workspace,
|
||
delete_host_workspace,
|
||
load_host_workspace_catalog,
|
||
rename_host_workspace,
|
||
resolve_host_workspace,
|
||
set_default_host_workspace,
|
||
)
|
||
from utils.host_workspace_debug import write_host_workspace_debug
|
||
from server.utils_common import log_conn_diag
|
||
import server.state as state
|
||
from modules.i18n import tr
|
||
@status_bp.route('/api/app/version')
|
||
@api_login_required
|
||
def get_app_version_info():
|
||
project_root = Path(__file__).resolve().parent.parent.parent
|
||
version_code, version_name = _parse_android_version_from_gradle(project_root)
|
||
apk_path = _resolve_android_apk_path(project_root)
|
||
apk_exists = apk_path.exists()
|
||
file_size = apk_path.stat().st_size if apk_exists else 0
|
||
published_at = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime(apk_path.stat().st_mtime)) if apk_exists else None
|
||
changelog = _load_app_changelog(project_root)
|
||
apk_url = request.url_root.rstrip("/") + "/api/app/apk/latest"
|
||
|
||
current_code_raw = request.args.get("currentVersionCode", "").strip()
|
||
current_code = int(current_code_raw) if current_code_raw.isdigit() else None
|
||
has_update = (version_code > current_code) if current_code is not None else None
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"data": {
|
||
"latestVersionCode": version_code,
|
||
"latestVersionName": version_name,
|
||
"apkUrl": apk_url,
|
||
"apkSha256": "",
|
||
"fileSizeBytes": file_size,
|
||
"changelog": changelog,
|
||
"forceUpdate": False,
|
||
"publishedAt": published_at,
|
||
"minSupportedVersionCode": 1,
|
||
"apkExists": apk_exists,
|
||
"hasUpdate": has_update,
|
||
}
|
||
})
|
||
|
||
@status_bp.route('/api/app/apk/latest')
|
||
def download_latest_apk():
|
||
# 安全:未认证全量下载 130MB+ 文件是带宽放大攻击面(2026-09-02 审计);
|
||
# 与 /api/app/version 一致要求登录,并加来源限流。
|
||
from server.auth_helpers import is_logged_in
|
||
if not is_logged_in():
|
||
return jsonify({"success": False, "error": tr("auth.not_logged_in")}), 401
|
||
from server.security import check_rate_limit, get_client_ip
|
||
limited, retry_after = check_rate_limit("apk_download", 10, 60, get_client_ip())
|
||
if limited:
|
||
return jsonify({"success": False, "error": "rate_limited", "retry_after": retry_after}), 429
|
||
project_root = Path(__file__).resolve().parent.parent.parent
|
||
apk_path = _resolve_android_apk_path(project_root)
|
||
if not apk_path.exists():
|
||
return jsonify({"success": False, "error": tr("status_app.apk_not_found")}), 404
|
||
return send_file(
|
||
apk_path,
|
||
as_attachment=True,
|
||
download_name="cyj-agent-latest.apk",
|
||
mimetype="application/vnd.android.package-archive",
|
||
conditional=False,
|
||
etag=False,
|
||
max_age=0,
|
||
)
|