13 KiB
13 KiB
安全问题分阶段修复计划
修复原则
- 安全第一:每个阶段修复后必须充分测试,确保不影响现有功能
- 最小改动:优先选择改动最小、风险最低的方案
- 可回滚:每个阶段独立提交,出问题可以快速回滚
- 渐进式:从简单到复杂,从低风险到高风险
第一阶段:明确 Bug 修复(低风险)
目标:修复明确的代码错误,不涉及架构变更
1.1 ENVI 主流程返回值缺失 ✅ 已完成
文件:backend/app/services/envi_service.py:1630
修改:
# 修改前
if error:
raise RuntimeError(error)
# 修改后
if error:
raise RuntimeError(error)
return record # 添加此行
风险评估:🟢 极低
- 只添加一行返回语句
- 不改变任何逻辑
- 修复了明确的 bug(成功时应该返回 record)
测试计划:
- 运行一个完整的 D-InSAR 工作流(dinsar_custom)
- 检查任务状态是否正确标记为 SUCCESS
- 检查日志是否包含
workflow=dinsar_custom duration=XXXs - 验证前端是否正确显示任务完成
1.2 失败计数重复累加
文件:backend/app/services/envi_service.py:1717, 1727
修改:
# 修改前(1717 行)
except OSError as e:
task_failed += 1
failed += 1 # 删除此行
task_status = f"error: {e}"
# 修改后
except OSError as e:
task_failed += 1
task_status = f"error: {e}"
# 修改前(1727 行)
except OSError as e:
task_failed += 1
failed += 1 # 删除此行
task_status = f"error: {e}"
# 修改后
except OSError as e:
task_failed += 1
task_status = f"error: {e}"
# 保留 1730 行的汇总
failed += task_failed # 只在这里累加一次
风险评估:🟢 极低
- 只删除重复的累加语句
- 不改变业务逻辑
- 修复统计数据不准确的问题
测试计划:
- 运行
extract_disp_results功能 - 故意触发一些失败(如权限问题)
- 验证返回的
failed计数是否准确(不翻倍)
1.3 PowerShell 编码问题
文件:scripts/start_app.ps1:388
修改:
# 修改前
$NewConfContent | Set-Content -LiteralPath "$NginxConfPath" -NoNewline
# 修改后
$NewConfContent | Set-Content -LiteralPath "$NginxConfPath" -NoNewline -Encoding UTF8
风险评估:🟢 极低
- 只添加编码参数
- 确保 nginx.conf 使用 UTF-8 编码
- 避免 PowerShell 5 默认 UTF-16 导致的问题
测试计划:
- 在 PowerShell 5 环境运行启动脚本
- 检查生成的 nginx.conf 文件编码(使用
file命令或文本编辑器) - 验证 Nginx 是否能正常启动和解析配置
第一阶段提交:
fix: 修复 ENVI 工作流返回值缺失和统计计数错误
- 修复 run_workflow 成功时不返回 record 的问题
- 修复 extract_disp_results 失败计数重复累加
- 修复 PowerShell 脚本 nginx.conf 编码问题
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
第二阶段:防御性增强(中风险)
目标:添加保护措施,不改变现有业务逻辑
2.1 AOI token 容量上限
文件:backend/app/routers/dependencies.py
修改策略:
- 添加环境变量配置
AOI_TOKEN_MAX_STORE_SIZE(默认 1000) - 在
_store_aoi_token中添加容量检查 - 达到上限时删除最旧的 100 个 token
实现:
# 在 dependencies.py 顶部添加
AOI_TOKEN_MAX_STORE_SIZE = read_int_env(
"AOI_TOKEN_MAX_STORE_SIZE",
1000,
minimum=100,
maximum=10000,
)
# 修改 _store_aoi_token 函数
async def _store_aoi_token(aoi_wkt: str, feature_collection: Dict[str, Any]) -> str:
token = uuid.uuid4().hex
now = time.monotonic()
async with _AOI_TOKEN_LOCK:
_cleanup_expired_aoi_tokens(now)
# 检查容量上限
if len(_AOI_TOKEN_STORE) >= AOI_TOKEN_MAX_STORE_SIZE:
# 删除最旧的 token(按 expires_at 排序)
oldest_tokens = sorted(
_AOI_TOKEN_STORE.items(),
key=lambda x: x[1]["expires_at"]
)[:100]
for old_token, _ in oldest_tokens:
_AOI_TOKEN_STORE.pop(old_token, None)
# 记录日志
print(f"[INFO] AOI token store reached limit, cleaned {len(oldest_tokens)} oldest tokens")
_AOI_TOKEN_STORE[token] = {
"aoi_wkt": aoi_wkt,
"aoi_geojson": feature_collection,
"expires_at": now + AOI_TOKEN_TTL_SECONDS,
}
return token
风险评估:🟡 中等
- 添加了新的容量限制逻辑
- 可能影响高并发场景下的 AOI 查询
- 但不改变现有 API 接口
测试计划:
- 创建 1000+ 个不同的 AOI token
- 验证内存占用是否稳定(不超过预期)
- 检查日志是否有 token 清理记录
- 验证被清理的 token 是否无法再使用
- 验证新创建的 token 仍然可用
2.2 AOI 解析异常处理
文件:backend/app/routers/dependencies.py
修改策略: 在所有 AOI 解析函数中添加统一的异常处理
实现位置:
_parse_geojson_to_wkt调用处_parse_shapefile_to_wkt调用处- 文件上传端点
实现:
# 在相关端点中添加 try-except
try:
aoi_wkt = _parse_geojson_to_wkt(geojson_data)
except (ValueError, KeyError, TypeError) as e:
raise HTTPException(
status_code=400,
detail=f"GeoJSON 格式错误: {str(e)}"
)
except Exception as e:
logger.error(f"AOI 解析失败: {e}", exc_info=True)
raise HTTPException(
status_code=500,
detail="AOI 解析失败,请联系管理员"
)
风险评估:🟡 中等
- 改变了错误响应码(500 → 400)
- 可能影响前端错误处理逻辑
- 但提升了 API 语义正确性
测试计划:
- 上传无效的 GeoJSON 文件
- 验证是否返回 400 而非 500
- 验证错误消息是否清晰
- 验证前端是否能正确处理 400 错误
2.3 路径归属判断优化
文件:backend/app/services/data_service.py:530
修改策略:
添加辅助函数 is_subpath,使用 os.path.commonpath 进行准确判断
实现:
# 在 data_service.py 顶部添加辅助函数
def is_subpath(child: str, parent: str) -> bool:
"""检查 child 是否是 parent 的子路径"""
try:
child_abs = os.path.abspath(child)
parent_abs = os.path.abspath(parent)
# 确保父路径以分隔符结尾,避免前缀误匹配
if not parent_abs.endswith(os.sep):
parent_abs += os.sep
if not child_abs.endswith(os.sep) and os.path.isdir(child_abs):
child_abs += os.sep
# 使用 commonpath 判断
common = os.path.commonpath([child_abs, parent_abs])
return common == parent_abs.rstrip(os.sep)
except (ValueError, TypeError):
return False
# 修改 530 行
# 修改前
if not any(file_path_abs.startswith(root) for root in radar_roots):
continue
# 修改后
if not any(is_subpath(file_path_abs, root) for root in radar_roots):
continue
风险评估:🟡 中等
- 改变了路径判断逻辑
- 可能影响文件扫描结果
- 但修复了潜在的安全问题
测试计划:
- 在
MONITOR_RADAR_DIRS旁边创建MONITOR_RADAR_DIRS_backup目录 - 放入测试文件
- 运行数据扫描
- 验证 backup 目录的文件不会被误判为合法
第二阶段提交:
feat: 添加 AOI token 容量限制和异常处理增强
- 添加 AOI_TOKEN_MAX_STORE_SIZE 配置(默认 1000)
- 达到上限时自动清理最旧的 token
- 统一 AOI 解析异常处理(400 vs 500)
- 优化路径归属判断,避免前缀误匹配
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
第三阶段:安全加固(高风险)
目标:解决权限和安全问题,可能涉及架构调整
3.1 解包安全校验增强
文件:scripts/unpack_archives.py
修改策略:
- 修改
_is_safe_tar_member接收 member 对象而非字符串 - 添加符号链接和硬链接检查
- 使用逐个提取替代
extractall
实现:
def _is_safe_tar_member(member):
"""检查 tar 成员是否安全(包括符号链接检查)"""
# 检查路径
norm_name = os.path.normpath(member.name)
if os.path.isabs(norm_name):
return False
if norm_name.startswith("..") or norm_name.startswith("../") or norm_name.startswith("..\\"):
return False
# 检查符号链接和硬链接
if member.issym() or member.islnk():
link_target = member.linkname
if os.path.isabs(link_target) or link_target.startswith(".."):
return False
return True
def _validate_tar_members(tar_obj, archive_path):
for member in tar_obj.getmembers():
if not _is_safe_tar_member(member):
raise IOError(f"unsafe tar entry detected: {member.name} in {archive_path}")
def _safe_extract_tar(tar_obj, output_dir):
"""安全地提取 tar 文件"""
for member in tar_obj.getmembers():
if not _is_safe_tar_member(member):
raise IOError(f"unsafe tar entry: {member.name}")
# 逐个提取
tar_obj.extract(member, path=output_dir)
# 在 181 行替换
with tarfile.open(archive_path, "r:*") as tar:
_validate_tar_members(tar, archive_path)
_safe_extract_tar(tar, output_dir) # 替换 tar.extractall
风险评估:🟡 中等
- 改变了解包逻辑
- 可能影响解包性能(逐个提取 vs 批量提取)
- 但提升了安全性
测试计划:
- 创建包含符号链接的 tar 文件
- 验证是否被拒绝解包
- 创建正常的 tar 文件
- 验证是否能正常解包
- 对比解包性能(前后差异)
3.2 只读用户权限控制(暂缓)
状态:⏸️ 暂缓到第四阶段
原因:
- 涉及权限系统重构
- 需要修改多个端点
- 可能影响前端调用逻辑
- 需要更充分的测试
备选方案:
- 方案 A:添加权限检查依赖
- 方案 B:配置控制(允许/禁止只读用户触发构建)
- 方案 C:改为 POST 端点(需要前端配合)
决策点:
- 是否有只读用户在使用系统?
- 只读用户是否需要查看预览图?
- 是否可以接受只读用户触发构建?
第三阶段提交:
security: 增强 tar 解包安全校验
- 添加符号链接和硬链接检查
- 使用逐个提取替代 extractall
- 防止目录逃逸和文件覆盖攻击
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
第四阶段:权限重构(待定)
目标:解决只读用户权限问题
前置条件:
- 第一、二、三阶段全部完成并稳定运行
- 明确只读用户的使用场景和需求
- 前端团队配合(如果需要修改 API 调用)
待讨论问题:
- 系统中是否有只读用户?
- 只读用户的典型使用场景是什么?
- 是否可以接受只读用户无法查看预览图?
- 是否需要前端配合修改?
修复时间表
| 阶段 | 预计时间 | 风险等级 | 依赖 |
|---|---|---|---|
| 第一阶段 | 1 天 | 🟢 低 | 无 |
| 第二阶段 | 2-3 天 | 🟡 中 | 第一阶段完成 |
| 第三阶段 | 2-3 天 | 🟡 中 | 第二阶段完成 |
| 第四阶段 | 待定 | 🔴 高 | 需求确认 |
回滚计划
每个阶段独立提交,出现问题时可以:
-
快速回滚:
git revert <commit-hash> -
部分回滚: 如果只有某个修复有问题,可以单独回滚该文件:
git checkout <previous-commit> -- <file-path> git commit -m "revert: 回滚 <file-path> 的修改" -
紧急回滚: 如果影响生产环境,立即回滚到上一个稳定版本:
git reset --hard <last-stable-commit>
监控指标
修复后需要监控的关键指标:
第一阶段
- ENVI 工作流成功率(应该提升)
- 任务状态准确性
- 统计数据准确性
第二阶段
- AOI token 内存占用(应该稳定)
- 400 vs 500 错误比例(400 应该增加)
- 路径扫描结果准确性
第三阶段
- 解包成功率(应该不变)
- 解包性能(可能略微下降)
- 安全事件(应该为 0)
总结
这个分阶段修复计划:
- ✅ 从低风险到高风险渐进式修复
- ✅ 每个阶段独立可测试、可回滚
- ✅ 优先修复明确的 bug,暂缓架构性改动
- ✅ 保留了灵活性,可以根据实际情况调整
建议:先完成第一阶段,充分测试后再决定是否继续第二阶段。