Skip to content

Commit b425504

Browse files
sunjh2222sundebiaoWangDaBenShixingxianluwangbochao789
authored
feat: LazyCraft安全加固 (#82)
* feat: 添加互联网功能开关机制 * 调整内置的模型清单 * 更新内置模型清单以、解除上传模型不可推理限制(并增加模型首次启动标识)、增加推理引擎是否正常检测功能 * fix(docCenter): 自动生成帮助文档导航 * refactor: 显卡数量选择变成option选项,去除readme中的无用image * fix: 将代码编辑器的文件改为vs名称 * fix: 新增提示词 * feat:在云服务中添加key的时候加入一个提醒 * feat:暂存 * fix: 修复默认联网模式的状态,新增部分测试案例、参数提取器修复模型选择 * fix:越权修复 * fix:越权修复 * feat:解决创建画布应用接口出现正在编辑中 * 更新子模块版本 * refactor: 更新推理服务界面,优化显卡数量选择和工具提示,删除不再使用的密钥交换接口 * fix: docx/xlsx/pptx类型数据检测为zip类型 * merge main * 更新子模块版本 * fix: docx/xlsx/pptx类型数据检测为zip类型 * fix:画布数据库连接检查 * chore: update API endpoints in environment files and enhance user agreement component with scroll position tracking * fix: 修复bug * fix:越权bug修复 * fix:修改Python文件上传提示信息 * feat:env files * feat:env files * fix(inferenceService): 修改弹层状态 --------- Co-authored-by: sundebiao <sundebiao@sensetime.com> Co-authored-by: wangbochao.vendor <15830881993@163.com> Co-authored-by: xingxianlu <xingxianlu.vendor@sensetime> Co-authored-by: wangbochao789 <wangbochao.vendor@sensetime.com> Co-authored-by: sunjinghua.vendor <sunjinghua.vendor@sensetime.com> Co-authored-by: 王博超 <wangbochao.vendor@60281529M.local>
1 parent 7439092 commit b425504

49 files changed

Lines changed: 1080 additions & 1517 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

back/src/core/asset_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ def _has_no_assets(self, tenant_id, account_id):
141141
return False
142142

143143
# 检查只有 tenant_id 的模型
144-
if tenant_id:
144+
if tenant_id and not account_id:
145145
odict = OrderedDict()
146146
odict["tenant_id"] = str(tenant_id)
147147
for modelcls in TENANT_ONLY_MODELS:

back/src/core/file_service.py

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import os
1818
import shutil
1919
import zipfile
20-
20+
import logging
2121
import sqlalchemy
2222

2323
from libs.filetools import FileTools
@@ -180,6 +180,7 @@ def add_knowledge_files(self, knowledge_base_id, file_ids):
180180
)
181181

182182
file_record.file_path = new_path
183+
file_record.tenant_id = knowledge_base.tenant_id
183184
file_record.used = True
184185
file_record.knowledge_base_id = knowledge_base_id
185186
file_record.updated_at = TimeTools.get_china_now()
@@ -270,10 +271,10 @@ def get_pagination_files(self, args):
270271
)
271272

272273
def batch_delete_files(self, file_ids):
273-
"""批量删除文件的数据库记录
274+
"""批量删除文件的数据库记录和物理文件
274275
275276
删除文件在数据库中的记录,并恢复租户的存储空间使用量。
276-
注意:目前不删除实际的文件,以防同样 MD5 的文件被误删
277+
同时删除物理文件,但会检查是否有其他记录使用相同的文件路径,避免误删
277278
278279
Args:
279280
file_ids: 要删除的文件 ID 列表。
@@ -284,6 +285,9 @@ def batch_delete_files(self, file_ids):
284285
if not file_ids:
285286
return 0 # 或者抛出一个适当的异常
286287

288+
# 先获取所有要删除的文件记录,以便删除物理文件
289+
files_to_delete = db.session.query(FileRecord).filter(FileRecord.id.in_(file_ids)).all()
290+
287291
# 使用数据库查询来计算文件大小
288292
total_size = (
289293
db.session.query(sqlalchemy.func.sum(FileRecord.size))
@@ -294,8 +298,30 @@ def batch_delete_files(self, file_ids):
294298
# 恢复已使用的存储空间
295299
Tenant.restore_used_storage(self.current_tenant_id, total_size)
296300

297-
# 使用批量删除
298-
# 目前没有删除文件实体,以防同样md5的文件被误删; 后续可以优化,节省磁盘空间
301+
# 删除物理文件(在删除数据库记录之前)
302+
for file_record in files_to_delete:
303+
file_path = file_record.file_path
304+
if file_path:
305+
# 检查是否有其他 FileRecord 使用相同的文件路径
306+
# 只检查当前租户的记录,且 tenant_id 不为空
307+
other_records = db.session.query(FileRecord).filter(
308+
FileRecord.file_path == file_path,
309+
FileRecord.id.notin_(file_ids),
310+
FileRecord.tenant_id == self.current_tenant_id
311+
).count()
312+
313+
# 如果没有其他记录使用该文件,则删除物理文件
314+
if other_records == 0 and os.path.exists(file_path):
315+
try:
316+
if os.path.isfile(file_path):
317+
os.remove(file_path)
318+
elif os.path.isdir(file_path):
319+
shutil.rmtree(file_path, ignore_errors=True)
320+
except Exception as e:
321+
# 记录错误但不中断删除流程
322+
logging.warning(f"删除文件失败: {file_path}, 错误: {e}")
323+
324+
# 使用批量删除数据库记录
299325
delete_stmt = sqlalchemy.delete(FileRecord).where(FileRecord.id.in_(file_ids))
300326
result = db.session.execute(delete_stmt)
301327
deleted_count = result.rowcount

back/src/core/handle_error.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515

1616
import sys
1717

18-
from flask import current_app, got_request_exception
18+
from flask import Response, current_app, got_request_exception
1919
from flask_restful import Api, http_status_message
2020
from werkzeug.datastructures import Headers
2121
from werkzeug.exceptions import HTTPException
@@ -26,6 +26,15 @@ class HandleErrorApi(Api):
2626
提供自定义的错误处理机制,统一处理各种异常并返回标准化的错误响应。
2727
"""
2828

29+
def make_response(self, data, *args, **kwargs):
30+
"""重写 make_response 方法,确保 Response 对象不会被序列化为 JSON。
31+
32+
当 data 是 Response 对象时,直接返回,不进行 JSON 序列化。
33+
"""
34+
if isinstance(data, Response):
35+
return data
36+
return super().make_response(data, *args, **kwargs)
37+
2938
def _create_error_data(self, code, message, status_code):
3039
"""创建标准化的错误数据结构。
3140

back/src/parts/apikey/apikey_api.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
from libs.login import login_required
2727
from models.model_account import Tenant
2828
from parts.apikey.apikey_service import ApikeyService
29-
from parts.apikey.model import ApiKeyStatus
29+
from parts.apikey.model import ApiKey, ApiKeyStatus
3030
from parts.app.app_service import AppService
3131
from parts.app.node_run.app_run_service import AppRunService
3232
from parts.urls import api
@@ -46,6 +46,7 @@ def get(self):
4646
Raises:
4747
CommonError: 当查询失败时抛出
4848
"""
49+
self.check_can_read()
4950
result = ApikeyService.query(current_user.id) # 根据当前用户查询其名下的apikey
5051
tenslist = Tenant.query.all()
5152

@@ -101,6 +102,9 @@ def post(self):
101102
for tid in tenant_id_list:
102103
if tid not in tenant_ids:
103104
return {"message": f"空间ID {tid} 不属于当前用户的空间"}, 400
105+
# 检查当前用户是否对该空间有写权限
106+
if not current_user.can_write_in_tenant(tid):
107+
return {"message": f"您对空间ID {tid} 没有写权限"}, 403
104108
self.check_can_write()
105109
result = ApikeyService.create_new(
106110
user_id=current_user.id,
@@ -131,7 +135,10 @@ def delete(self):
131135
id = args.get("id", None)
132136
if id is None:
133137
return {"message": "id参数不能为空"}, 400
134-
self.check_can_write()
138+
api_key = ApiKey.query.get(id)
139+
if api_key is None:
140+
return {"message": "API Key不存在"}, 400
141+
self.check_can_admin_object(api_key)
135142
ApikeyService.delete_api_key(id=id, user_id=current_user.id)
136143

137144
return {"result": "success"}, 204
@@ -164,6 +171,10 @@ def put(self):
164171
]
165172
if status not in allowed_statuses:
166173
return {"message": f"不支持的状态: {status}"}, 400
174+
api_key = ApiKey.query.get(id)
175+
if api_key is None:
176+
return {"message": "API Key不存在"}, 400
177+
self.check_can_write_object(api_key)
167178
result = ApikeyService.update_status(
168179
id=id, user_id=current_user.id, new_status=status
169180
)

back/src/parts/app/app_api.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,9 @@ def get(self):
119119
)
120120
args = parser.parse_args()
121121

122+
# 权限校验:列表接口需要 read 权限
123+
self.check_can_read()
124+
122125
client = AppService()
123126
pagination = client.get_paginate_apps(current_user, args)
124127
response = marshal(pagination, fields.app_pagination_fields)
@@ -214,6 +217,8 @@ def post(self):
214217
"enable_api", type=inputs.boolean, location="json", required=False
215218
)
216219
args = parser.parse_args()
220+
221+
self.check_can_read()
217222
client = AppService()
218223
pagination = client.get_paginate_apps(current_user, args)
219224
response = marshal(pagination, fields.app_pagination_fields)
@@ -244,6 +249,9 @@ def get(self, app_id):
244249
"""
245250
# 如果是通过子画布的接口来访问,会拿不到app_model
246251
app_model = AppService().get_app(app_id, raise_error=False)
252+
if app_model:
253+
self.check_can_read_object(app_model)
254+
247255
return marshal(app_model, fields.app_detail_fields)
248256

249257
@login_required
@@ -490,6 +498,9 @@ def get(self, app_id):
490498
args = parser.parse_args()
491499

492500
app_model = AppService().get_app(app_id, raise_error=False)
501+
if app_model:
502+
self.check_can_write_object(app_model)
503+
493504
result = marshal(app_model, fields.app_export_fields)
494505
workflow = Workflow.default_getone(app_id, args["version"])
495506
result["graph"] = workflow.nested_graph_dict if workflow else {}
@@ -573,6 +584,10 @@ def post(self, app_id):
573584
parser = reqparse.RequestParser()
574585
parser.add_argument("file", type=FileStorage, required=True, location="files")
575586
uploaded_file = parser.parse_args()["file"]
587+
588+
app_model = AppService().get_app(app_id, raise_error=False)
589+
if app_model:
590+
self.check_can_write_object(app_model)
576591

577592
rawdata = json.loads(uploaded_file.read())
578593

@@ -622,6 +637,7 @@ def get(self):
622637
) # mine/group/builtin/already
623638
args = parser.parse_args()
624639

640+
self.check_can_read()
625641
client = TemplateService()
626642
app_pagination = client.get_paginate_apps(current_user, args)
627643
if not app_pagination:
@@ -644,6 +660,7 @@ def get(self, app_id):
644660
ValueError: 当模板不存在时抛出
645661
"""
646662
template = TemplateService().get_app(app_id)
663+
self.check_can_read_object(template)
647664
return marshal(template, fields.app_detail_fields)
648665

649666
@login_required
@@ -729,6 +746,9 @@ def post(self):
729746

730747
client = TemplateService()
731748
template = client.get_app(args["id"])
749+
if template:
750+
self.check_can_read_object(template)
751+
732752
app = client.convert_to_app(current_user, template, args)
733753
LogService().add(Module.APP_STORE, Action.CREATE_APP_TMP, name=app.name)
734754
return app, 201
@@ -1176,7 +1196,11 @@ def get(self, app_id):
11761196

11771197

11781198
class CheckVersionsCount(Resource):
1199+
@login_required
11791200
def get(self, app_id):
1201+
app_model = AppService().get_app(app_id, raise_error=False)
1202+
if app_model:
1203+
self.check_can_write_object(app_model)
11801204
version_count = AppService().get_version_count(app_id)
11811205
message = ""
11821206
is_over_limit = False

back/src/parts/app/app_service.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,7 @@ def convert_to_app(self, account, source, args):
553553
"""
554554
target = App(**source.to_copy_dict())
555555
target.created_by = account.id
556+
target.tenant_id = account.current_tenant_id
556557
target.enable_site = target.enable_api = False
557558
target.status = "draft"
558559
target.name = args.get("name") or target.name

back/src/parts/app/node_run/node_base.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,7 +878,66 @@ def __init__(self, nodedata, context):
878878
self.db_name = self.get_data().get("payload__db_name")
879879
self.options_str = self.get_data().get("payload__options_str")
880880

881+
def _validate_db_params(self) -> None:
882+
"""校验数据库连接参数,确保不会通过连接 URL 携带多余命令或进行URL注入。
883+
884+
由于这些参数会被直接拼接到数据库连接URL中,需要校验:
885+
1. 禁止SQL注入相关的字符(分号、换行、注释符等)
886+
2. 禁止URL中的特殊字符,防止URL注入
887+
"""
888+
# SQL注入相关的禁止字符
889+
sql_forbidden_tokens = [";", "\n", "\r", "--", "/*", "*/"]
890+
891+
# URL特殊字符(用于URL注入防护)
892+
# 注意:host可能包含IPv6地址(如[::1]),需要特殊处理
893+
url_forbidden_for_user = ["@", ":", "/", "?", "#"]
894+
url_forbidden_for_host = ["@", "/", "?", "#"] # 允许:和[](IPv6)
895+
url_forbidden_for_db_name = ["/", "?", "#"]
896+
url_forbidden_for_options = ["#"] # options_str是查询字符串,允许&和=
897+
898+
def _check_sql_injection(name: str, value: str) -> None:
899+
"""检查SQL注入相关的字符"""
900+
if value is None:
901+
return
902+
s = str(value)
903+
for token in sql_forbidden_tokens:
904+
if token in s:
905+
raise ValueError(f"非法数据库连接参数 {name} 中包含非法字符: {token}")
906+
907+
def _check_url_special_chars(name: str, value: str, forbidden_chars: list) -> None:
908+
"""检查URL特殊字符"""
909+
if value is None:
910+
return
911+
s = str(value)
912+
for char in forbidden_chars:
913+
if char in s:
914+
raise ValueError(f"非法数据库连接参数 {name} 中包含URL特殊字符: {char}")
915+
916+
# 校验所有字段的SQL注入风险
917+
_check_sql_injection("user", self.user)
918+
_check_sql_injection("host", self.host)
919+
_check_sql_injection("db_name", self.db_name)
920+
_check_sql_injection("options_str", self.options_str)
921+
922+
# 校验URL特殊字符(防止URL注入)
923+
_check_url_special_chars("user", self.user, url_forbidden_for_user)
924+
_check_url_special_chars("host", self.host, url_forbidden_for_host)
925+
_check_url_special_chars("db_name", self.db_name, url_forbidden_for_db_name)
926+
_check_url_special_chars("options_str", self.options_str, url_forbidden_for_options)
927+
928+
# 端口必须是数字
929+
if self.port is not None:
930+
port_str = str(self.port)
931+
if not port_str.isdigit():
932+
raise ValueError("非法数据库连接参数 port 必须为数字")
933+
port_num = int(port_str)
934+
if port_num < 1 or port_num > 65535:
935+
raise ValueError("非法数据库连接参数 port 必须在1-65535范围内")
936+
881937
def _to_dict(self, result):
938+
# 在序列化前再次校验,确保数据安全
939+
self._validate_db_params()
940+
882941
result["args"] = {
883942
"db_type": self.db_type,
884943
"user": self.user,

0 commit comments

Comments
 (0)