fix(security): 代码审查安全加固 — 三批次修复(6H/13M/12L)
第一批(快速修复): - H6: main.py --debug 默认值 True→False,防止 Werkzeug RCE - M2+M3: /search 增加 validate_query + top_k 范围限制(1-50) - M4: context_count 范围限制(0-10) + 异常捕获 - L3: assert → raise RuntimeError(生产环境 API Key 检查) - H1: SSE 错误事件移除 traceback 字段 第二批(安全加固): - H2+H3: 文档接口路径遍历 realpath 校验 + 文件类型/大小限制 - H4+H5: 批量上传文件大小检查 - M6: LIKE 查询通配符转义 - M1: 37 处 str(e) 异常信息统一脱敏(6 文件) - M5: CORS 生产环境限制来源 - M7: SESSION_MANAGER None 保护(503) - M11: subprocess 参数注入防护(白名单 + -- 分隔符) 第三批(架构改进): - M8+M9: 提取 JSON 解析共享工具(extract_json_object/list) - M10: Prompt 注入检测防御(prompt_guard.py) - M12: 解析器文件大小限制(Excel 50MB/TXT 20MB/PDF 100MB) - M13: 全局单例竞态条件双重检查锁定(engine/bm25/intent_analyzer)
This commit is contained in:
@@ -96,6 +96,24 @@ def safe_filename(filename: str) -> str:
|
||||
|
||||
return safe_name
|
||||
|
||||
|
||||
def _validate_doc_path(doc_path: str, documents_path: str) -> str:
|
||||
"""
|
||||
校验文档路径是否在允许目录内,防止路径遍历攻击。
|
||||
|
||||
Returns:
|
||||
解析后的安全绝对路径
|
||||
Raises:
|
||||
ValueError: 路径不合法
|
||||
"""
|
||||
filepath = os.path.join(documents_path, doc_path)
|
||||
real_path = os.path.realpath(filepath)
|
||||
real_base = os.path.realpath(documents_path)
|
||||
if not real_path.startswith(real_base + os.sep) and real_path != real_base:
|
||||
raise ValueError("非法路径")
|
||||
return real_path
|
||||
|
||||
|
||||
# 延迟初始化缓存
|
||||
_kb_manager = None
|
||||
_kb_checked = False
|
||||
@@ -160,6 +178,10 @@ def serve_document_file(doc_path: str) -> Tuple[Any, int]:
|
||||
from flask import send_from_directory
|
||||
|
||||
filepath = os.path.join(DOCUMENTS_PATH, doc_path)
|
||||
try:
|
||||
filepath = _validate_doc_path(doc_path, DOCUMENTS_PATH)
|
||||
except ValueError:
|
||||
return jsonify({"error": "非法路径"}), 403
|
||||
if not os.path.exists(filepath):
|
||||
return jsonify({"error": "文件不存在"}), 404
|
||||
|
||||
@@ -329,7 +351,8 @@ def upload_document() -> Tuple[Any, int]:
|
||||
sync_service.process_change(change)
|
||||
sync_status = "已保存并添加到向量库"
|
||||
except Exception as e:
|
||||
sync_status = f"已保存,向量化失败: {str(e)}"
|
||||
logger.warning(f"向量化失败: {e}")
|
||||
sync_status = "已保存,向量化失败"
|
||||
|
||||
return success_response(
|
||||
data={
|
||||
@@ -404,6 +427,18 @@ def batch_upload_documents() -> Tuple[Any, int]:
|
||||
})
|
||||
continue
|
||||
|
||||
# 文件大小校验
|
||||
file.seek(0, 2)
|
||||
file_size = file.tell()
|
||||
file.seek(0)
|
||||
if file_size > MAX_FILE_SIZE:
|
||||
results.append({
|
||||
"filename": file.filename,
|
||||
"status": "error",
|
||||
"message": f"文件过大(最大 {MAX_FILE_SIZE // 1024 // 1024}MB)"
|
||||
})
|
||||
continue
|
||||
|
||||
try:
|
||||
original_filename = file.filename
|
||||
ext = os.path.splitext(original_filename)[1].lower()
|
||||
@@ -464,10 +499,11 @@ def batch_upload_documents() -> Tuple[Any, int]:
|
||||
"replaced": replaced
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"上传处理异常: {e}")
|
||||
results.append({
|
||||
"filename": file.filename,
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
"message": "上传处理失败"
|
||||
})
|
||||
|
||||
return success_response(
|
||||
@@ -644,12 +680,27 @@ def update_document(doc_path: str) -> Tuple[Any, int]:
|
||||
if file.filename == '':
|
||||
return jsonify({"error": "没有选择文件"}), 400
|
||||
|
||||
# 文件类型校验
|
||||
ext = os.path.splitext(file.filename)[1].lower()
|
||||
if ext not in ALLOWED_EXTENSIONS:
|
||||
return jsonify({"error": f"不支持的文件类型: {ext}"}), 400
|
||||
|
||||
# 文件大小校验
|
||||
file.seek(0, 2) # 跳到文件末尾获取大小
|
||||
file_size = file.tell()
|
||||
file.seek(0) # 回到文件开头
|
||||
if file_size > MAX_FILE_SIZE:
|
||||
return jsonify({"error": f"文件过大(最大 {MAX_FILE_SIZE // 1024 // 1024}MB)"}), 400
|
||||
|
||||
# 解析路径
|
||||
parts = doc_path.split('/')
|
||||
if len(parts) < 2:
|
||||
return jsonify({"error": "无效的文档路径"}), 400
|
||||
|
||||
filepath = os.path.join(DOCUMENTS_PATH, doc_path)
|
||||
try:
|
||||
filepath = _validate_doc_path(doc_path, DOCUMENTS_PATH)
|
||||
except ValueError:
|
||||
return jsonify({"error": "非法路径"}), 403
|
||||
if not os.path.exists(filepath):
|
||||
return jsonify({"error": "文件不存在"}), 404
|
||||
|
||||
@@ -711,7 +762,10 @@ def delete_document(doc_path: str) -> Tuple[Any, int]:
|
||||
# 目录名即向量库名
|
||||
collection = subdir
|
||||
|
||||
filepath = os.path.join(DOCUMENTS_PATH, doc_path)
|
||||
try:
|
||||
filepath = _validate_doc_path(doc_path, DOCUMENTS_PATH)
|
||||
except ValueError:
|
||||
return jsonify({"error": "非法路径"}), 403
|
||||
if not os.path.exists(filepath):
|
||||
return jsonify({"error": "文件不存在"}), 404
|
||||
|
||||
@@ -730,7 +784,8 @@ def delete_document(doc_path: str) -> Tuple[Any, int]:
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"删除失败: {str(e)}"}), 500
|
||||
logger.error(f"删除文档异常: {e}")
|
||||
return jsonify({"error": "删除失败,请稍后重试"}), 500
|
||||
|
||||
|
||||
@document_bp.route('/documents/<path:doc_path>/chunks', methods=['GET'])
|
||||
@@ -817,7 +872,10 @@ def preview_document(doc_path: str) -> Tuple[Any, int]:
|
||||
|
||||
# 查询参数
|
||||
chunk_index_str = request.args.get('chunk_index')
|
||||
context_count = int(request.args.get('context', 2))
|
||||
try:
|
||||
context_count = max(0, min(int(request.args.get('context', 2)), 10))
|
||||
except (ValueError, TypeError):
|
||||
context_count = 2
|
||||
|
||||
# 获取所有切片
|
||||
all_chunks = kb_manager.get_document_chunks(collection, filename)
|
||||
@@ -1082,5 +1140,5 @@ def delete_chunks_by_source() -> Tuple[Any, int]:
|
||||
"message": f"已删除 {deleted_count} 个切片"
|
||||
})
|
||||
except Exception as e:
|
||||
logger.error(f"批量删除切片失败: {e}")
|
||||
return jsonify({"error": str(e)}), 500
|
||||
logger.error(f"操作异常: {e}")
|
||||
return jsonify({"error": "操作失败,请稍后重试"}), 500
|
||||
|
||||
Reference in New Issue
Block a user