关于 security-logging-alerting-failures
security-logging-alerting-failures 是针对 OWASP A09:2025 的安全审查技能,供审计 FastAPI 和 Flask 应用的日志与告警实践。它提供检查清单、结构化日志建议、告警阈值及自动扫描命令。
主要功能
- 01检查日志注入和敏感信息泄露
- 02核对认证与权限事件日志
- 03评估结构化日志及 SIEM 兼容性
- 04检查告警规则和事件响应
- 05提供 CI/CD 自动扫描命令
适用场景
- 01审查 FastAPI 或 Flask 的安全日志
- 02查找日志中的用户输入与机密数据
- 03核验认证失败和管理操作是否记录
- 04评估生产环境告警覆盖情况
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ scholarly360/ owasp-top10-web-skills --skill security-logging-alerting-failures安装后在 Claude Code 中输入/security-logging-alerting-failures确认已加载。
附带文件references/alerting-platforms.mdreferences/remediation-patterns.md
详细介绍
A09:2025 — 安全日志记录与告警失效
OWASP 排名: #9(社区连续第三轮投票选出)
2025 年更名: 新增“告警”——只记录日志而不发出告警,安全价值极低。
统计数据: 5 个 CWE、723 个 CVE、3.91% 平均发生率
涵盖的 CWE: CWE-778、CWE-117、CWE-532、CWE-223、CWE-390
此类别涵盖的内容
| 失效类型 | 描述 |
|---|---|
| 日志记录不足 | 未记录可审计事件(登录、访问控制失败、验证错误) |
| 日志注入 | 用户输入未经转义直接写入日志消息,导致日志伪造 |
| 日志中的敏感数据 | 密码、令牌、PII、卡号被写入日志输出 |
| 遗漏安全相关信息 | 记录错误/警告时缺少上下文(IP、用户、时间戳、操作) |
| 无告警 | 虽然存在日志,但没有阈值或规则,也没有平台将其发送给事件响应团队 |
审计清单 — FastAPI 和 Flask
按顺序检查各个部分。对于每项,请标注:通过、失败或不适用。
1. 日志注入(CWE-117)
绝不能将用户可控的输入未经处理直接写入日志语句。
需要检查:
# VULNERABLE — newline injection, ANSI escape injection
logger.info(f"User logged in: {request.form['username']}")
logger.warning("Failed login for: " + username)
# SAFE — sanitize before logging
import re
safe_username = re.sub(r'[\r\n\x1b]', '_', username)
logger.info("Failed login for: %s", safe_username)
清单:
- 日志调用中不使用 f-string 或
+拼接原始请求数据 - 从所有用户提供的日志值中去除换行符(
\r、\n) - 去除 ANSI 转义码(
\x1b[)(防止终端注入) - 使用结构化日志记录(JSON 格式)——通过字段分隔从设计上防止注入
2. 日志中的敏感数据(CWE-532)
日志输出中绝不能出现机密信息,即使在 DEBUG 级别也不例外。
需要标记的模式:
# VULNERABLE
logger.debug(f"Auth token: {token}")
logger.info(f"Processing payment for card: {card_number}")
app.logger.debug(request.json) # May contain password field
print(f"Password reset for {email}, token={reset_token}")
# SAFE
logger.info("Auth token validated for user_id=%s", user_id)
logger.info("Payment processed for user_id=%s, last4=%s", user_id, card_number[-4:])
需要扫描的内容:
- 不记录以下内容:
password、passwd、secret、token、api_key、authorization标头值 - 不记录以下内容:信用卡号、SSN、完整的电子邮箱和密码组合
- 不在未筛选字段的情况下完整记录
request.json/request.form - 生产代码中没有包含敏感变量的
print()语句 - CI/CD 中通过 Bandit 规则
B506(及类似规则)的检查
3. 日志记录不足(CWE-778)
以下事件必须生成包含完整上下文的日志条目。
必需记录的日志事件:
| 事件 | 最低必需字段 |
|---|---|
| 身份验证失败 | 时间戳、IP、尝试使用的用户名、用户代理 |
| 身份验证成功 | 时间戳、IP、user_id、方法(密码/OAuth/MFA) |
| 授权失败(403) | 时间戳、IP、user_id、尝试访问的资源、HTTP 方法 |
| 输入验证失败 | 时间戳、IP、user_id、字段名(敏感信息不记录其值)、错误 |
| 高价值交易 | 时间戳、user_id、操作、资源 ID、更改前/后的状态 |
| 账户变更(密码重置、角色变更) | 时间戳、actor_user_id、target_user_id、变更类型 |
| 管理员操作 | 时间戳、admin_user_id、操作、受影响的资源 |
FastAPI 实现模式:
import logging
import structlog # recommended for structured logs
log = structlog.get_logger()
# In auth endpoint
@router.post("/login")
async def login(credentials: LoginSchema, request: Request):
user = await authenticate(credentials)
if not user:
log.warning(
"auth.failed",
ip=request.client.host,
username=credentials.username, # OK — username is not a secret
user_agent=request.headers.get("user-agent"),
)
raise HTTPException(status_code=401, detail="Invalid credentials")
log.info("auth.success", user_id=user.id, ip=request.client.host)
Flask 实现模式:
from flask import request
import logging
@app.route("/login", methods=["POST"])
def login():
user = authenticate(request.form["username"], request.form["password"])
if not user:
app.logger.warning(
"auth.failed ip=%s username=%s ua=%s",
request.remote_addr,
request.form.get("username"),
request.user_agent.string,
)
return {"error": "Invalid credentials"}, 401
清单:
- 所有 401/403 响应都有对应的
logger.warning()或logger.error()调用 - 日志条目包含:时间戳(由
logging自动添加)、源 IP、用户标识符 - 记录密码重置和账户修改流程
- 记录管理面板操作,并包含操作人的身份
- 高价值业务交易会生成审计日志条目
4. 结构化日志记录与 SIEM 兼容性
日志格式必须可供可观测性平台(ELK、Splunk、Datadog、Sentry)解析。
FastAPI — structlog + JSON:
# main.py
import structlog
import logging
structlog.configure(
processors=[
structlog.processors.TimeStamper(fmt="iso"),
structlog.stdlib.add_log_level,
structlog.processors.JSONRenderer(), # machine-parseable
],
wrapper_class=structlog.BoundLogger,
logger_factory=structlog.PrintLoggerFactory(),
)
Flask — python-json-logger:
from pythonjsonlogger import jsonlogger
handler = logging.StreamHandler()
handler.setFormatter(jsonlogger.JsonFormatter(
fmt="%(asctime)s %(levelname)s %(name)s %(message)s"
))
app.logger.addHandler(handler)
清单:
- 生产环境中的日志输出为 JSON 格式(而非纯文本)
- 生产环境中的日志级别不是
DEBUG(以免泄露过多信息) - 日志发送到集中式平台:Sentry、ELK、Splunk、Datadog、CloudWatch
- 已制定日志保留政策,并符合合规要求(例如至少保留 90 天)
5. 告警集成
只有日志记录而没有告警 = 故障悄无声息。请确认已启用监控。
Sentry(异常告警):
# FastAPI
import sentry_sdk
from sentry_sdk.integrations.fastapi import FastApiIntegration
sentry_sdk.init(dsn=settings.SENTRY_DSN, integrations=[FastApiIntegration()])
# Flask
from sentry_sdk.integrations.flask import FlaskIntegration
sentry_sdk.init(dsn=settings.SENTRY_DSN, integrations=[FlaskIntegration()])
建议的告警阈值:
| 信号 | 告警触发条件 |
|---|---|
| 登录失败 | 5 分钟内同一用户名登录失败次数 >10 |
| 登录失败 | 5 分钟内来自同一 IP 的登录失败次数 >100 |
| 403 响应 | 1 分钟内来自同一 user_id 的响应次数 >50 |
| 500 错误 | 任意 1 分钟窗口内的请求中 >5% |
| 新建管理员账户 | 每次发生时立即告警 |
| 权限提升 | 每次发生时立即告警 |
检查清单:
- Sentry 或同类工具已初始化,并在生产环境中接收事件
- 速率限制违规会生成告警事件(而不只是返回 HTTP 429 响应)
- 重复的身份验证失败会触发告警(暴力破解检测)
- 每种告警类型都有对应的告警运行手册/操作手册
- 在高价值区域部署了蜜罐令牌(近乎零误报的入侵检测)
自动化扫描
将这些工具纳入 CI/CD 流程:
# Static analysis — catches log injection patterns, sensitive data in logs
bandit -r . -t B106,B107,B110 --severity-level medium
# Search for raw request data in log calls
grep -rn "logger\.\(info\|warning\|error\|debug\)" . \
| grep -E "request\.(form|json|args|data|headers)\[" \
| grep -v "\.get("
# Search for potential sensitive field logging
grep -rn "logger\|logging\|print" . \
| grep -iE "password|passwd|secret|token|api_key|card|ssn|cvv"
重要 CWE 参考
| CWE | 名称 | Python 风险 |
|---|---|---|
| CWE-778 | 日志记录不足 | 身份验证/授权失败时没有日志记录 |
| CWE-117 | 日志注入 | 在 logger 调用中使用 f-string 插入用户输入 |
| CWE-532 | 日志中的敏感信息 | 调试输出中包含密码/令牌 |
| CWE-223 | 遗漏安全相关信息 | 日志缺少 IP/用户上下文 |
| CWE-390 | 检测到错误条件但未采取操作 | 捕获异常后将其吞掉,且未记录日志 |
参考文件
references/remediation-patterns.md— 各 CWE 的完整修复前后代码references/alerting-platforms.md— Sentry、ELK、Splunk、Datadog 集成指南
当用户需要超出检查清单范围的详细实现帮助时,请阅读这些文件。