Files
insar-management-system-v2/docs/archive/SECURITY_FIX_PLAN.md
T

13 KiB
Raw Blame History

安全问题分阶段修复计划

修复原则

  1. 安全第一:每个阶段修复后必须充分测试,确保不影响现有功能
  2. 最小改动:优先选择改动最小、风险最低的方案
  3. 可回滚:每个阶段独立提交,出问题可以快速回滚
  4. 渐进式:从简单到复杂,从低风险到高风险

第一阶段:明确 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

修改策略

  1. 添加环境变量配置 AOI_TOKEN_MAX_STORE_SIZE(默认 1000
  2. _store_aoi_token 中添加容量检查
  3. 达到上限时删除最旧的 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

修改策略

  1. 修改 _is_safe_tar_member 接收 member 对象而非字符串
  2. 添加符号链接和硬链接检查
  3. 使用逐个提取替代 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. 是否可以接受只读用户无法查看预览图?
  4. 是否需要前端配合修改?

修复时间表

阶段 预计时间 风险等级 依赖
第一阶段 1 天 🟢
第二阶段 2-3 天 🟡 第一阶段完成
第三阶段 2-3 天 🟡 第二阶段完成
第四阶段 待定 🔴 需求确认

回滚计划

每个阶段独立提交,出现问题时可以:

  1. 快速回滚

    git revert <commit-hash>
    
  2. 部分回滚: 如果只有某个修复有问题,可以单独回滚该文件:

    git checkout <previous-commit> -- <file-path>
    git commit -m "revert: 回滚 <file-path> 的修改"
    
  3. 紧急回滚: 如果影响生产环境,立即回滚到上一个稳定版本:

    git reset --hard <last-stable-commit>
    

监控指标

修复后需要监控的关键指标:

第一阶段

  • ENVI 工作流成功率(应该提升)
  • 任务状态准确性
  • 统计数据准确性

第二阶段

  • AOI token 内存占用(应该稳定)
  • 400 vs 500 错误比例(400 应该增加)
  • 路径扫描结果准确性

第三阶段

  • 解包成功率(应该不变)
  • 解包性能(可能略微下降)
  • 安全事件(应该为 0

总结

这个分阶段修复计划:

  • 从低风险到高风险渐进式修复
  • 每个阶段独立可测试、可回滚
  • 优先修复明确的 bug,暂缓架构性改动
  • 保留了灵活性,可以根据实际情况调整

建议:先完成第一阶段,充分测试后再决定是否继续第二阶段。