在 CI/CD 流水线中接入 DAST 扫描时,最常见的配置错误之一就是让扫描器以"未登录"的匿名身份去测试一个需要认证才能访问的应用。这种"未登录即扫描"的做法通常会产生两种代价高昂的结果:要么扫描器只覆盖了登录页和少数公开接口,大量业务功能完全处于扫描盲区;要么因为扫描器在未认证状态下反复触发重定向、登录跳转和会话超时,导致报告中充满与真实风险无关的误报。无论哪种情况,团队都会对扫描结果失去信任,最终把 DAST 从安全门禁降级为"仅生成报告但不阻断"的摆设。
要解决这个问题,核心在于为 DAST 扫描器配置一个可复用的认证会话,并让这个会话贯穿"登录态注入—扫描执行—结果回归"的完整链路。本文以 OWASP ZAP 在 GitLab CI 中的接入为例,介绍如何通过环境变量或脚本注入凭据、处理会话 Cookie 与 CSRF Token,并给出验证扫描确实使用了认证会话的具体方法。

为什么"未登录扫描"必然产生失真结果
在配置认证之前,先要理解 DAST 扫描器在未认证状态下的行为模式。以 OWASP ZAP 为例,当它访问一个需要登录的应用时,通常会遇到以下情况:
- 应用返回 302 重定向到登录页,ZAP 会跟随重定向并记录登录页的响应。
- 如果登录页包含表单,ZAP 的主动扫描可能会尝试向表单字段注入测试载荷,但这些测试针对的是"登录表单本身"而非业务功能。
- 对于需要会话的 API 接口,未认证请求通常返回 401 或 403,ZAP 会将这些状态码记录为"发现",但无法深入测试接口背后的参数处理逻辑。
这种扫描结果的直接后果是:覆盖率严重不足。一个典型的业务应用,登录后的功能页面可能占全部可测试面的 80% 以上,未认证扫描只能覆盖剩余的公开部分。更隐蔽的问题是,未认证扫描产生的"漏洞"往往集中在登录页的 CSRF、公开搜索框的反射型 XSS 等低价值目标上,而真正危险的越权访问、业务逻辑漏洞、登录后接口的 SQL 注入却完全无法被发现。
GitLab 官方文档在介绍 DAST 配置时也明确指出,DAST 分析器必须与应用进行认证,才能获得完整的测试覆盖。这意味着认证会话不是可选项,而是 DAST 扫描结果有效性的前提条件。
认证会话配置的最小流水线示例
下面以一个典型的 GitLab CI 流水线为例,展示如何为 OWASP ZAP 配置认证会话。这个示例假设目标应用是部署在测试环境中的 Web 应用,登录表单包含用户名、密码和隐藏的 CSRF Token 字段。
准备阶段:安全传递凭据
凭据绝不能硬编码在 .gitlab-ci.yml 或代码仓库中。正确做法是使用 GitLab CI/CD 的受保护变量(Protected Variables)或外部密钥管理服务。在 GitLab 项目中,进入 Settings → CI/CD → Variables,添加以下变量:
这三个变量都应勾选 Protected 和 Masked 选项,确保只有受保护分支的流水线才能使用,且变量值在日志中会被打码。
配置阶段:登录态注入
GitLab 的 DAST 分析器支持通过 CI/CD 变量配置认证行为。在 .gitlab-ci.yml 中,一个最小化的认证 DAST 任务如下:
dast:
stage: dast
image: docker:stable
services:
- docker:stable-dind
variables:
DAST_WEBSITE: "$DAST_TARGET_URL"
DAST_AUTH_URL: "$DAST_TARGET_URL/login"
DAST_AUTH_USERNAME_FIELD: "username"
DAST_AUTH_PASSWORD_FIELD: "password"
DAST_AUTH_USERNAME: "$DAST_USERNAME"
DAST_AUTH_PASSWORD: "$DAST_PASSWORD"
DAST_AUTH_METHOD: "form"
DAST_BROWSER_SCAN: "true"
DAST_ZAP_OPTIONS: "-config globalexcludeurl.url_list.url='regex:.*logout.*'"
script:
- docker run --rm -v "$PWD:/zap/wrk/:rw" -t ghcr.io/zaproxy/zaproxy zap-baseline.py
-t "$DAST_TARGET_URL"
-r dast_report.html
artifacts:
paths:
- dast_report.html
reports:
dast: gl-dast-report.json这里有几个关键点需要说明:
DAST_AUTH_METHOD: "form"告诉分析器使用表单登录方式。如果应用使用 HTTP Basic 认证或基于 Header 的认证,需要相应调整为http_basic或header。DAST_AUTH_URL指向登录页面,分析器会先访问该页面,提取表单中的 CSRF Token(如果存在),再提交登录请求。DAST_ZAP_OPTIONS中的globalexcludeurl配置用于排除登出 URL,防止扫描过程中意外登出导致会话失效。这个配置在认证扫描中非常重要,因为 ZAP 的爬虫可能会主动点击"退出登录"链接。
处理 CSRF Token 与会话 Cookie
如果登录表单包含 CSRF Token,GitLab 的 DAST 分析器默认会尝试从登录页 HTML 中提取并自动提交。但在某些自定义表单中,Token 可能通过 JavaScript 动态加载,或者字段名不符合常规。这时需要额外配置 DAST_AUTH_CSRF_TOKEN_FIELD 变量来指定 Token 字段名:
DAST_AUTH_CSRF_TOKEN_FIELD: "authenticity_token"对于会话 Cookie 的保持,GitLab 提供了 DAST_AUTH_COOKIES 变量,可以指定逗号分隔的 Cookie 名称列表,分析器会确保这些 Cookie 在扫描过程中始终被携带。这在应用依赖多个会话 Cookie 时特别有用:
DAST_AUTH_COOKIES: "session_id,csrf_token,user_prefs"需要注意的是,如果应用使用较新的 SameSite 或 Secure 属性 Cookie,或依赖 HttpOnly 标志,ZAP 默认会正确处理,但建议在扫描前通过 ZAP 的桌面版或 API 手动验证一次 Cookie 的传递行为。
验证扫描确实使用了认证会话
配置完成后,最关键的一步是验证扫描器确实以认证身份执行了测试。很多团队配置了认证变量,但扫描结果仍然显示未认证状态,原因通常是登录失败、Token 提取错误或会话在扫描中途失效。
方法一:检查扫描报告中的响应状态码分布
认证扫描与未认证扫描最直观的差异体现在 HTTP 状态码分布上。在 ZAP 生成的 HTML 报告中,查看"Alert"或"Response"统计部分。如果认证成功,应该看到大量 200 响应,而不是集中在 302、401、403。具体来说:
- 未认证扫描:大量 302(重定向到登录页)、401(未授权)、403(禁止访问)。
- 认证扫描:200 响应占比显著提升,且扫描到的 URL 列表中包含登录后可见的业务路径。
方法二:在扫描目标中设置"认证哨兵"
一种更可靠的验证方式是在目标应用中设置一个仅认证用户可访问的哨兵页面或接口。例如,在测试环境中添加一个返回用户信息 JSON 的接口 /api/me,只有携带有效会话 Cookie 的请求才能返回 200 和用户数据。然后在 ZAP 的扫描配置中,将 /api/me 加入爬虫的起始 URL 列表。扫描结束后,检查 ZAP 的"Sites"树中 /api/me 的响应状态:
- 如果响应是 200 且包含用户数据,说明认证会话有效。
- 如果响应是 401 或 302,说明认证失败。
这个方法的优点是结果明确、可自动化断言。在 CI 流水线中,可以用一个简单的脚本检查 ZAP 生成的报告 JSON:
#!/bin/bash
# 检查认证哨兵是否被成功访问
if grep -q '"url":".*/api/me".*"status":200' gl-dast-report.json; then
echo "认证会话验证通过"
else
echo "认证会话验证失败:哨兵接口未返回 200"
exit 1
fi方法三:对比未认证与认证扫描的差异
在首次配置时,建议分别运行一次未认证扫描和一次认证扫描,对比两份报告。差异会非常明显:认证扫描的 URL 数量通常会增加数倍,且发现的漏洞类型会从"登录页 CSRF"转向"业务逻辑越权""登录后接口的注入"等更有价值的问题。这种对比既能验证配置正确性,也能让团队直观理解认证扫描的价值。
扫描结果关联到修复回归
认证扫描配置完成后,还需要解决"扫描结果如何推动修复"的问题。仅仅在流水线中生成一份报告是不够的,必须将结果关联到缺陷跟踪和回归验证流程。
在 GitLab 中,DAST 扫描结果可以通过 reports: dast 关键字上传为安全报告,并在合并请求中直接展示。开发者可以在 MR 页面看到新增的漏洞,并直接在页面上创建 Issue 或关联已有 Issue。
对于修复回归,建议在流水线中增加一个"验证阶段":当开发者在 MR 中标记某个漏洞已修复后,流水线重新运行 DAST 扫描,并对比两次扫描的漏洞列表。具体做法是使用 GitLab 的 gl-dast-report.json 文件,通过脚本比较两次扫描的漏洞签名(通常使用漏洞的 URL + 告警类型作为唯一标识):
import json
with open('baseline_dast_report.json') as f:
baseline = json.load(f)
with open('current_dast_report.json') as f:
current = json.load(f)
baseline_vulns = {(v['url'], v['alert']) for v in baseline['vulnerabilities']}
current_vulns = {(v['url'], v['alert']) for v in current['vulnerabilities']}
fixed = baseline_vulns - current_vulns
new = current_vulns - baseline_vulns
print(f"已修复漏洞: {len(fixed)}")
print(f"新增漏洞: {len(new)}")这个脚本可以作为流水线中的一个独立任务,在 MR 关闭前运行。如果 fixed 集合中包含开发者在 MR 描述中声明的漏洞,则回归验证通过;如果 new 集合不为空,则说明修复引入了新的安全问题,流水线应阻断合并。
误报处置与配置回退
认证扫描虽然能显著降低误报率,但并不意味着零误报。常见的误报来源包括:
- 扫描器对业务逻辑的误解:例如,扫描器将"修改个人资料后返回 200"误判为"越权修改"。
- 测试环境数据差异:测试环境中的脏数据导致某些请求返回了与生产环境不同的响应。
- 会话状态漂移:扫描过程中其他测试任务修改了数据,导致某些请求失败。
对于误报,建议建立"漏洞分类"流程:在 GitLab 安全报告中,允许开发者将漏洞标记为"误报"或"需要人工验证",并填写理由。这个分类信息应作为流水线门禁的输入——只有被标记为"已确认"的漏洞才阻断合并。
配置回退方面,如果认证扫描导致流水线频繁失败或扫描时间过长,可以考虑以下策略:
- 将 DAST 从"阻塞门禁"降级为"非阻塞报告",在 MR 中展示结果但不阻断合并。
- 将认证扫描从每次提交触发改为每日定时任务或合并请求前手动触发。
- 在
DAST_ZAP_OPTIONS中调整扫描策略,例如降低主动扫描强度(-t参数)或排除某些高误报率的告警类型。
需要注意的是,GitLab 的代理型 DAST 分析器在 GitLab 16.9 中已被弃用,并在后续版本中移除。当前推荐使用基于浏览器的 DAST 分析器(DAST_BROWSER_SCAN: "true"),它通过真实浏览器执行登录流程,能更准确地处理现代 JavaScript 应用中的认证逻辑。
结语
为 DAST 扫描配置认证会话,本质上是在回答一个安全假设问题:你的应用有多少功能是真正暴露给匿名用户的? 如果答案是"很少",那么未认证扫描的结果就几乎没有参考价值。通过环境变量注入凭据、显式处理 CSRF Token 与会话 Cookie、设置认证哨兵验证扫描身份、并将结果关联到修复回归,你可以让 DAST 从"形式上的安全门禁"转变为"真正能发现业务风险的有效工具"。这个过程不需要复杂的定制开发,只需要理解认证链路中的几个关键环节,并用可复现的流水线配置把它们固定下来。
- CI/CD
- DAST
- it2021
- it2021.com
- OWASP
- tag





宁夏 中卫 1F
认证哨兵这个思路挺实用,回头试试
上海 上海 2F
想问下这种认证会话配置对单页应用也适用吗