威胁模型:拥有普通账号的已登录用户攻击服务器。完整审计报告见
_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)。
162 lines
6.0 KiB
Python
162 lines
6.0 KiB
Python
from __future__ import annotations
|
|
from server.chat import chat_bp
|
|
import json, time
|
|
from datetime import datetime
|
|
from typing import Dict, Any, Optional
|
|
from pathlib import Path
|
|
from io import BytesIO
|
|
import zipfile
|
|
import os
|
|
|
|
from flask import Blueprint, jsonify, request, session, send_file
|
|
from werkzeug.utils import secure_filename
|
|
from werkzeug.exceptions import RequestEntityTooLarge
|
|
import secrets
|
|
|
|
from config import MAX_UPLOAD_SIZE, OUTPUT_FORMATS
|
|
from modules.personalization_manager import (
|
|
load_personalization_config,
|
|
resolve_context_compression_settings,
|
|
save_personalization_config,
|
|
RECENT_CONVERSATIONS_PROMPT_LIMIT_MIN,
|
|
RECENT_CONVERSATIONS_PROMPT_LIMIT_MAX,
|
|
)
|
|
from modules.skills_manager import (
|
|
get_skills_catalog,
|
|
infer_private_skills_dir,
|
|
merge_enabled_skills,
|
|
sync_workspace_skills,
|
|
)
|
|
from modules.upload_security import UploadSecurityError
|
|
from modules.host_sandbox_policy import load_policy, save_policy
|
|
from modules.user_manager import UserWorkspace
|
|
from core.web_terminal import WebTerminal
|
|
from config.model_profiles import get_model_context_window
|
|
|
|
from server.auth_helpers import api_login_required, resolve_admin_policy, get_current_user_record, get_current_username
|
|
from server.context import with_terminal, get_gui_manager, get_upload_guard, build_upload_error_response, ensure_conversation_loaded, get_or_create_usage_tracker
|
|
from server.security import rate_limited, prune_socket_tokens
|
|
from server.utils_common import debug_log
|
|
from server.state import PROJECT_MAX_STORAGE_MB, pending_socket_tokens, SOCKET_TOKEN_TTL_SECONDS
|
|
from server.state import tool_approval_manager, user_question_manager
|
|
from server.extensions import socketio
|
|
from server.monitor import get_cached_monitor_snapshot
|
|
|
|
from modules.i18n import tr
|
|
|
|
UPLOAD_FOLDER_NAME = ".astrion/user_upload"
|
|
@chat_bp.route('/api/memory', methods=['GET'])
|
|
@api_login_required
|
|
@with_terminal
|
|
def api_memory_entries(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
|
"""返回主/任务记忆条目列表,供虚拟显示器加载"""
|
|
memory_type = request.args.get('type', 'main')
|
|
if memory_type not in ('main', 'task'):
|
|
return jsonify({"success": False, "error": tr("chat_misc.invalid_memory_type")}), 400
|
|
try:
|
|
entries = terminal.memory_manager._read_entries(memory_type) # type: ignore
|
|
return jsonify({"success": True, "type": memory_type, "entries": entries})
|
|
except Exception as exc:
|
|
return jsonify({"success": False, "error": str(exc)}), 500
|
|
|
|
@chat_bp.route('/api/gui/monitor_snapshot', methods=['GET'])
|
|
@api_login_required
|
|
def get_monitor_snapshot_api():
|
|
execution_id = request.args.get('executionId') or request.args.get('execution_id') or request.args.get('id')
|
|
if not execution_id:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': tr('chat_misc.missing_execution_id')
|
|
}), 400
|
|
stage = (request.args.get('stage') or 'before').lower()
|
|
if stage not in {'before', 'after'}:
|
|
stage = 'before'
|
|
snapshot = get_cached_monitor_snapshot(execution_id, stage, username=get_current_username())
|
|
if not snapshot:
|
|
return jsonify({
|
|
'success': False,
|
|
'error': tr('chat_misc.snapshot_not_found')
|
|
}), 404
|
|
return jsonify({
|
|
'success': True,
|
|
'snapshot': snapshot,
|
|
'stage': stage
|
|
})
|
|
|
|
@chat_bp.route('/api/focused')
|
|
@api_login_required
|
|
@with_terminal
|
|
def get_focused_files(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
|
"""聚焦功能已废弃,返回空列表保持接口兼容。"""
|
|
return jsonify({})
|
|
|
|
@chat_bp.route('/api/todo-list')
|
|
@api_login_required
|
|
@with_terminal
|
|
def get_todo_list(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
|
"""获取当前待办列表"""
|
|
todo_snapshot = terminal.context_manager.get_todo_snapshot()
|
|
return jsonify({
|
|
"success": True,
|
|
"data": todo_snapshot
|
|
})
|
|
|
|
@chat_bp.route('/api/tool-settings', methods=['GET', 'POST'])
|
|
@api_login_required
|
|
@with_terminal
|
|
def tool_settings(terminal: WebTerminal, workspace: UserWorkspace, username: str):
|
|
"""获取或更新工具启用状态"""
|
|
if request.method == 'GET':
|
|
snapshot = terminal.get_tool_settings_snapshot()
|
|
return jsonify({
|
|
"success": True,
|
|
"categories": snapshot
|
|
})
|
|
|
|
data = request.get_json() or {}
|
|
category = data.get('category')
|
|
if category is None:
|
|
return jsonify({
|
|
"success": False,
|
|
"error": tr("chat_misc.missing_category"),
|
|
"message": tr("chat_misc.missing_category_field")
|
|
}), 400
|
|
|
|
if 'enabled' not in data:
|
|
return jsonify({
|
|
"success": False,
|
|
"error": tr("chat_misc.missing_enabled"),
|
|
"message": tr("chat_misc.missing_enabled_field")
|
|
}), 400
|
|
|
|
try:
|
|
policy = resolve_admin_policy(get_current_user_record())
|
|
if policy.get("ui_blocks", {}).get("block_tool_toggle"):
|
|
return jsonify({
|
|
"success": False,
|
|
"error": tr("chat_misc.tool_toggle_admin_disabled"),
|
|
"message": tr("chat_misc.admin_forced_disabled")
|
|
}), 403
|
|
enabled = bool(data['enabled'])
|
|
forced = getattr(terminal, "admin_forced_category_states", {}) or {}
|
|
if isinstance(forced.get(category), bool) and forced[category] != enabled:
|
|
return jsonify({
|
|
"success": False,
|
|
"error": tr("chat_misc.category_forced"),
|
|
"message": tr("chat_misc.admin_forced_state")
|
|
}), 403
|
|
terminal.set_tool_category_enabled(category, enabled)
|
|
snapshot = terminal.get_tool_settings_snapshot()
|
|
socketio.emit('tool_settings_updated', {
|
|
'categories': snapshot
|
|
}, room=f"user_{username}")
|
|
return jsonify({
|
|
"success": True,
|
|
"categories": snapshot
|
|
})
|
|
except ValueError as exc:
|
|
return jsonify({
|
|
"success": False,
|
|
"error": str(exc)
|
|
}), 400
|