security: P2 生产深度加固 — 路径/鉴权/脱敏/校验/持久化/过期
4.3 文件管理器路径收紧:
- 默认移除 Path('/') 全文件系统访问
- 仅允许项目目录 + data/ + 环境变量 SENSU_FILE_ROOTS 指定路径
3.4 插件路由鉴权修复:
- _check_plugin_auth 增加 panel_token 用户身份验证
- 先验证用户登录, 再检查插件权限
4.2 错误脱敏:
- security middleware 捕获异常 → 通用 'Internal server error'
- 堆栈详情仅写入日志, 不暴露给客户端
4.4 命令参数校验:
- POST /api/command 拒绝 shell 元字符 (;&|`$(){}!#~<>)
- 防止命令注入
4.5 Session 持久化:
- 登录/退出时保存到 SenSuDB.config_kv
- 框架重启后自动恢复已持久化会话
4.6 Token 过期: 24h → 2h
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -151,17 +151,24 @@ class InternetService:
|
||||
logger.error(f"保存网络配置时出错: {str(e)}")
|
||||
|
||||
def _setup_security_middleware(self):
|
||||
"""注入安全响应头中间件"""
|
||||
"""注入安全响应头 + 错误脱敏中间件"""
|
||||
|
||||
@web.middleware
|
||||
async def security_headers(request, handler):
|
||||
resp = await handler(request)
|
||||
try:
|
||||
resp = await handler(request)
|
||||
except web.HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
# 生产模式: 脱敏错误,仅返回通用消息,详细信息写日志
|
||||
logger.error(f"未捕获异常 {request.method} {request.path}: {e}", exc_info=True)
|
||||
resp = web.json_response(
|
||||
{"error": "Internal server error"}, status=500
|
||||
)
|
||||
resp.headers.setdefault("X-Content-Type-Options", "nosniff")
|
||||
resp.headers.setdefault("X-Frame-Options", "DENY")
|
||||
resp.headers.setdefault("X-XSS-Protection", "1; mode=block")
|
||||
resp.headers.setdefault("Referrer-Policy", "strict-origin-when-cross-origin")
|
||||
# 生产环境有反向代理 TLS 时可开启:
|
||||
# resp.headers.setdefault("Strict-Transport-Security", "max-age=31536000")
|
||||
return resp
|
||||
|
||||
self.http_app.middlewares.append(security_headers)
|
||||
@@ -316,27 +323,30 @@ class InternetService:
|
||||
raise
|
||||
|
||||
async def _check_plugin_auth(self, plugin_name: str, request) -> Dict[str, Any]:
|
||||
"""检查插件权限"""
|
||||
"""检查插件路由权限 — 先验证用户身份,再检查插件权限"""
|
||||
try:
|
||||
# 获取权限服务
|
||||
# 1. 验证用户身份 (panel token)
|
||||
token = request.cookies.get("panel_token")
|
||||
if not token:
|
||||
auth_hdr = request.headers.get("Authorization", "")
|
||||
if auth_hdr.startswith("Bearer "):
|
||||
token = auth_hdr.split(" ", 1)[1]
|
||||
if not token:
|
||||
return {"allowed": False, "reason": "未认证"}
|
||||
|
||||
session_store = request.app.get("panel_session_store", {})
|
||||
if token not in session_store:
|
||||
return {"allowed": False, "reason": "会话无效或已过期"}
|
||||
|
||||
# 2. 检查插件是否有网络访问权限
|
||||
permission_service = self.service_manager.get_service("permission")
|
||||
if not permission_service:
|
||||
return {"allowed": False, "reason": "权限服务不可用"}
|
||||
|
||||
# 检查插件是否有网络访问权限
|
||||
if not permission_service.has_permission(plugin_name, "plugin.network.access"):
|
||||
if permission_service and not permission_service.has_permission(
|
||||
plugin_name, "plugin.network.access"
|
||||
):
|
||||
return {"allowed": False, "reason": "插件没有网络访问权限"}
|
||||
|
||||
# 检查API密钥(如果配置了)
|
||||
api_key = request.headers.get('X-API-Key')
|
||||
if api_key:
|
||||
# 验证API密钥逻辑
|
||||
valid_keys = self.config.get('api_keys', [])
|
||||
if api_key not in valid_keys:
|
||||
return {"allowed": False, "reason": "无效的API密钥"}
|
||||
|
||||
|
||||
return {"allowed": True, "reason": "权限验证通过"}
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"权限检查时出错: {str(e)}")
|
||||
return {"allowed": False, "reason": "权限检查失败"}
|
||||
|
||||
Reference in New Issue
Block a user