API 安全例外管理
Issue:#1897
概述
本文档描述 Open ACE 项目中管理 API 安全基线抑制(suppression)与例外的流程。
安全扫描器
API 安全扫描器(scripts/lint/api_security_scanner.py)检测 Flask 路由中的安全违规:
- SEC001:路由处理函数没有认证
- SEC002:带 ID 参数的路由缺少所有权检查
- SEC003:Blueprint 缺少
@before_request认证钩子
基线文件
安全抑制存放在 scripts/lint/security_baseline.json。当前基线条目:截至 2026-09-29 为 0 条(scripts/lint/security_baseline.json)。每条抑制都必须有完整的元数据:
{
"key": "SEC002|app/routes/workspace.py|/api/workspace/knowledge/<entry_id>",
"rule": "SEC002",
"file": "app/routes/workspace.py",
"line": 2038,
"endpoint": "/api/workspace/knowledge/<entry_id>",
"message": "Route ... has ID param(s) ['entry_id'] but no ownership check",
"metadata": {
"owner": "@team-backend",
"justification": "Reason why this suppression is necessary",
"reviewed_at": "2026-07-23",
"expires_at": "2027-01-23T00:00:00Z",
"risk_level": "low",
"test_coverage": "tests/integration/routes/test_example.py::test_example",
"alternative_controls": [
"List of alternative security controls in place"
]
}
}
必填元数据字段
- owner:负责该抑制的 GitHub 用户名或团队
- justification:清楚说明为什么需要这条例外
- test_coverage:验证该安全控制的测试文件与测试名
可选元数据字段
- reviewed_at:上次审查日期(ISO 格式 )
- expires_at:过期日期(ISO 格式)——过期后 CI 会失败
- risk_level:风险评估(low/medium/high)
- alternative_controls:已有安全控制的列表
- automated_check:关于自动化检查的元数据
新增一条抑制
步骤 1:登记例外
新增抑制之前,先确认:
- 你有设立该例外的正当理由
- 已有替代安全控制(例如工作区级访问控制)
- 已有或即将补充测试覆盖
步骤 2:使用 @security_annotated 装饰器
对于以非标准模式实现所有权检查的端点:
from app.auth.decorators import security_annotated
@workspace_bp.route("/resource/<int:resource_id>", methods=["GET"])
@security_annotated(reason="Ownership via get_user_resource + permission check")
def get_resource(resource_id):
# Ownership check implemented inline
resource = get_user_resource(user_id, resource_id)
if not resource:
return jsonify({"error": "Access denied"}), 403
...
步骤 3:更新基线
如果添加 @security_annotated 后扫描器仍标记该端点:
python scripts/lint/api_security_scanner.py --baseline > scripts/lint/security_baseline.json
步骤 4:补充元数据
编辑 scripts/lint/security_baseline.json,为新增抑制补齐 完整元数据。
步骤 5:校验
运行元数据校验:
python scripts/lint/validate_baseline_metadata.py
移除一条抑制
出现以下情况时应移除抑制:
- 代码已修复、安全问题已解决
- 端点已被移除
- 替代安全控制已不再必要
步骤 1:修复问题
实现适当的安全控制,或移除该端点。
步骤 2:重新生成基线
python scripts/lint/api_security_scanner.py --baseline > scripts/lint/security_baseline.json
步骤 3:校验
python scripts/lint/api_security_scanner.py
python scripts/lint/validate_baseline_metadata.py
CI 集成
安全扫描器在 CI 中对每个 pull request 运行:
- API Security Scanner:检测不在基线中的新违规
- Baseline Diff Check:检测基线变更(需要说明理由)
- Metadata Validation:确保所有抑制都有完整元数据
例外生命周期
每条抑制都遵循同一生命周期:带元数据新增 → 季度审查 → 续期或移除。本节是安全基线例外的权威流程——包括 skip-security-check PR 绕过标签——适用于所有基线指南;BANDIT_BASELINE.md 链接到此处,不再重复叙述。
季度审计
基线抑制按季度(1/4/7/10 月)审计:
- 检查
expires_at日期——过期后 CI 会失败 - 复核
owner与justification——确认仍然准确 - 验证
test_coverage——确保测试仍然通过 - 评估
alternative_controls——确认仍然有效
审计提醒目前靠人工——还没有定时 workflow 审计这些抑制(季度性的 false-positive-review.yml workflow 覆盖的是测试注解,不是安全例外)。
紧急绕过
紧急情况下,可在 PR 上使用 skip-security-check 标签:
gh pr edit --add-label skip-security-check
要求:
- 在 PR 描述中说明绕过原因
- 创建后续 Issue 跟踪需要修复的问题
- 经 Security Team 或 Admin 审批后方可合并
⚠️ 警告:skip-security-check 仅用于紧急情况,滥用将被记录审查。
功能开关
部分安全特性可以通过环境变量逐步启用:
ENFORCE_PROMPT_OWNERSHIP
控制提示词模板所有权校验:
true(默认):强制执行所有权检查false:仅记录日志,不拒绝请求(用于灰度发布)
一次性灰度方案(阶段划分、回滚步骤、成功标准)已迁至 docs/dev-notes/2026-09-29-enforce-prompt-ownership-rollout.md。
安全审查清单
审查带基线变更的 PR 时:
- 所有新增抑制都有完整元数据
-
owner是有效的 GitHub 用户/团队 -
justification清楚解释了该例外 -
test_coverage存在且测试的是该安全控制 -
expires_at已设置且不过分久远 -
alternative_controls已记录且有效 - PR 描述说明了为什么需要该例外
相关文档
- Issue #1897:API Security Baseline Cleanup
- API Security Scanner
- Baseline Metadata Validator
- Baseline Diff Checker
联系方式
关于 API 安全例外的问题:
- 安全团队:@security
- 后端团队:@team-backend