k6 + OWASP ZAP:把性能压测与安全扫描融合成一条流水线
跑过一次印象很深刻的压测之后我才真正意识到性能和安全这两件事根本不能分开看。当时我压的是一个文件上传服务并发数刚过300接口的响应时间就开始明显劣化按老经验这八成是连接池不够或者带宽占满了。但把线程栈抓出来一看服务端那个上传逻辑正在对文件名做异常复杂的正则解析每次上传都要重复编译请求一多CPU直接被打满。这不是性能问题这是一个可以被廉价触发的拒绝服务场景放在OWASP Top 10里叫资源耗尽型风险放在压测里叫吞吐量瓶颈。从那次之后我的负载测试脚本里永远会多带一个东西安全视角。这就是我今天想聊的话题怎么把k6和OWASP ZAP串起来让一次压测同时回答两个问题——系统能扛多少量扛量的时候有没有暴露出不该暴露的东西。这套组合其实很适合已经在做DevSecOps或者正在往这个方向靠的团队。k6负责把流量真实地打上去ZAP负责在这股流量里做安全层面的观察和扫描两者配合能把原本割裂的两个流程合并成一条流水线。文章后面会讲到具体的脚本写法、ZAP集成的方式、报告怎么看、以及我在实际项目里踩过的几个坑。1. 为什么非要把压测和安全测试放在同一条跑道上1.1 性能和安全的边界远比想象中模糊大多数团队对性能测试和安全测试的分工是清晰的压测归研发效能或者QA管安全扫描归安全组管。可现实里这两类问题的症状往往长得一模一样。举个例子一个登录接口突然在某个并发量下返回大量500性能工程师第一反应是数据库连接数不够。但如果你把错误响应体抓出来看一眼可能会发现那段堆栈信息把内部数据库地址、ORM版本号、甚至一段SQL都带了出来。性能问题戴着的面具底下是信息泄露。反过来也一样一次恶意攻击的流量特征和一个快速增长的压测场景从服务端日志上看几乎是同构的都是大量请求在短时间内集中到达都在挑战系统在极限负载下的行为方式。所以与其让两拨人各自为战不如让压测工具和安全扫描器在同一个流程里协同工作至少让安全扫描的输入流量跟真实压测的流量叠加在一起这样得出的结论才更接近生产环境的实际情况。1.2 k6配合OWASP ZAP到底能解决什么先明确一个边界k6不是安全测试工具OWASP ZAP也不是性能测试工具。k6负责的是生成持续、可度量、可重复的负载把一个系统的吞吐上限、延迟分布、错误率边界逼出来。ZAP负责的是站在攻击者的角度对Web应用做探测和扫描找漏洞比如SQL注入、XSS、路径遍历、不安全的上传校验。两者结合之后的想象力在于你可以在ZAP的被动扫描模式下直接分析k6跑批量的真实业务流量而不是扫描器自己编造的那些探测请求。因为ZAP的被动扫描会在代理层分析每一个经过它的请求和响应k6的压测流量本身就是一个很好的样本池。另一方面ZAP的主动扫描也可以单独跑但这时候如果能让ZAP扫描的目标服务同时处于k6压测负载之下你就能观察到一个问题接口在低负载下似乎能挡住的攻击在负载升高之后是否还能挡住。比如某个上传接口在空闲状态下对文件类型校验得很严格但并发上来之后由于代码里的缓存失效或者异常分支触发校验逻辑被跳过了这个状态用普通安全扫描是扫不出来的但在k6加压的过程中通过ZAP检测大概率能发现。这套集成解决的不是有了更好而是分开测会漏的问题。2. k6压测脚本设计别只想着一件事“能跑”2.1 先理解k6的压测模型再动手k6的脚本本质是一个JavaScript模块里面定义了一个默认导出函数这个函数就是虚拟用户VU反复执行的业务动作。k6的执行模型在于VU数量和迭代次数的调度不是靠并发线程池也跟你平时用的JMeter线程组思路不同。k6把压力分成两个维度每个VU是一个独立的循环整个测试的负载形态则通过options里的scenarios来定义。常见的写法是import http from k6/http; import { check, sleep } from k6; export const options { scenarios: { load_test: { executor: ramping-vus, startVUs: 0, stages: [ { duration: 2m, target: 200 }, { duration: 5m, target: 200 }, { duration: 2m, target: 0 }, ], gracefulRampDown: 30s, }, }, thresholds: { http_req_duration: [p(95)500, p(99)800], http_req_failed: [rate0.01], }, }; export default function () { const res http.get(https://api.example.com/health); check(res, { status is 200: (r) r.status 200, }); sleep(1); }这段脚本里最关键的是stages和thresholds。stages决定了整个压测过程中的负载形态thresholds则决定了压测是pass还是fail。很多人刚用k6时会忽视thresholds只把压测当成一个数据采集过程。实际上k6的设计哲学是压测是一个有明确断言指标的自动化过程当阈值被击穿时k6会返回非零退出码这个行为就是后面接CI流水线时的命门。2.2 业务场景别只压一个GET接口我之前见过不少团队搭建压测平台脚本里清一色的GET请求测出来的结果只能说这个静态资源服务器的性能不错跟实际业务系统没什么关系。真实业务场景至少应该包含一个完成的交易闭环登录拿token查询列表提交一条业务数据再带上文件上传、下载这类重I/O操作。k6的脚本可以用http.batch并发多个请求也可以用多次http调用模拟一个业务流程。下面这个例子是一个更接近真实的多步骤场景import http from k6/http; import { check } from k6; import { Trend } from k6/metrics; const loginTrend new Trend(login_duration); const uploadTrend new Trend(upload_duration); export default function () { const loginRes http.post(https://api.example.com/login, { username: testuser, password: testpass123, }); loginTrend.add(loginRes.timings.duration); check(loginRes, { login status 200: (r) r.status 200, login has token: (r) r.json(token) ! undefined, }); const token loginRes.json(token); const headers { Authorization: Bearer ${token}, }; const uploadBody http.file(open(./sample.pdf), sample.pdf, application/pdf); const uploadRes http.post(https://api.example.com/upload, uploadBody, { headers }); uploadTrend.add(uploadRes.timings.duration); check(uploadRes, { upload status 201: (r) r.status 201, }); }在这个脚本里登录和上传之间存在数据依赖也就是token。k6天然支持这种依赖因为每个VU都是独立执行default函数的每个VU用自己的token走完整的链路这比JMeter里用正则提取器做关联要直观很多。在压测报告里你还能通过自定义的Trend指标单独观察每个环节的耗时定位瓶颈在哪一步。2.3 参数化测试数据不然压测没意义压测数据如果只有一套比如所有请求都用同一个用户名、同一份文件、同一个订单号测出来的结果几乎一定会失真。因为真实系统中的数据库索引、缓存策略、锁粒度都是基于数据分散度设计的。k6的参数化数据可以用__VU和__ITER这两个内建变量来做简单的数据切片也可以直接读取一个JSON文件或者CSV文件让每个VU每次迭代拿到不同的数据。读取CSV的写法是import papaparse from https://jslib.k6.io/papaparse/5.1/papaparse.min.js; import { SharedArray } from k6/data; const csvData new SharedArray(users, function () { return papaparse.parse(open(./users.csv), { header: true }).data; }); export default function () { const user csvData[__VU % csvData.length]; // 使用 user.username / user.password 发请求 }SharedArray是k6在分享测试数据时一个很推荐的结构它把数据在多个VU之间共享降低内存占用同时又保证了每个VU可以按自己的节奏取用数据。这里有一个细节值得注意不要把数据读取写在default函数内部反复open文件这么做会让磁盘I/O成为压测的瓶颈把负载工具自身的性能问题混进被测系统的性能数据里。正确做法是在模块加载阶段把数据读进内存测试过程中只做切片。2.4 阈值不能只定响应时间阈值是k6里判断测试通过还是失败的规则。很多团队只会写http_req_duration的p95小于多少毫秒这远远不够。一个完整的阈值体系至少应该覆盖三个维度可用性、性能、资源趋势。export const options { thresholds: { http_req_failed: [rate0.001], http_req_duration: [avg300, p(95)500, p(99)1000], http_reqs: [rate2000], iteration_duration: [max5000], }, };http_req_failed这个指标经常被忽略但它其实是你判断系统是否稳定的第一道闸门。当错误率超过0.1%的时候后面的延迟指标再好看也没有意义因为系统已经在丢弃请求了。另外http_reqs可以设置一个最低吞吐量防止出现系统很稳但吞吐量低得离谱的假象。iteration_duration则保证整个业务流程一轮迭代不太慢避免单个VU因为某一步卡住而影响整体压测进度。3. 将OWASP ZAP集成到压测流程3.1 先把ZAP跑起来OWASP ZAP是一个开源的Web应用安全扫描器它既能做被动扫描也能做主动扫描还能当一个代理服务器拦截分析流量。这正好给了它与k6集成的接口。ZAP的启动方式很多最省事的是直接下载ZAP的发行包解压运行或者用Dockerdocker run -d --name zap \ -p 8090:8090 \ -v zap_data:/zap/data \ owasp/zap2docker-stable \ zap.sh -daemon -port 8090 -host 0.0.0.0启动参数里-daemon表示后台运行-port 8090是ZAP的代理监听端口-host必须设置成0.0.0.0否则容器外面访问不到。ZAP还有一个API端口默认是8080可以通过-api-port参数指定后面做自动化集成时需要用到。要注意的是ZAP容器里最好挂一个数据卷避免每次重启之后扫描结果和历史记录全丢了。3.2 集成方案一把k6的流量送进ZAP代理这是我最常用的做法而且实现成本很低。k6的http请求支持通过proxy参数或者环境变量K6_HTTP_PROXY代理出去。你把k6的代理指向ZAP的8090端口那么k6产生的每一条请求都会经过ZAPZAP的被动扫描器就会自动分析这些流量的请求和响应寻找安全漏洞。在k6脚本中启用代理的方式export K6_HTTP_PROXYhttp://127.0.0.1:8090 k6 run --vus 100 --duration 5m load-test.js或者直接在脚本里export const options { proxies: { http: http://127.0.0.1:8090, https: http://127.0.0.1:8090, }, };这种方案的思路很清楚ZAP本质上是一个中间人代理k6的流量从它这里过一遍ZAP就能在被动模式下完成大量检测不需要额外发起主动攻击。对于压测工程师来说唯一的成本就是给k6配一个代理地址。需要注意一点如果被测系统是HTTPSZAP需要能生成并信任它的CA证书否则连接会报证书错误。你需要在被测环境里安装ZAP的CA证书在本地调试的时候也可以临时把SSL verify关闭但生产环境的压测绝对不能这么干。3.3 集成方案二压测结束后触发ZAP主动扫描被动扫描能看到请求和响应里的常见问题比如缺少安全响应头、cookie缺少HttpOnly属性、敏感信息泄露等。但真正的深度漏洞检测还是依赖于主动扫描也就是让ZAP主动发送攻击payload去试探目标接口。主动扫描需要先知道目标应用的入口点。你可以直接用ZAP的API把需要扫描的URL传给ZAP也可以在ZAP的界面里设置上下文和站点然后调用扫描接口curl -X POST http://127.0.0.1:8080/JSON/ascan/action/scan/ \ -d urlhttp://target.example.com:8080/如果需要指定一个完整上下文可以先通过API定义context把urlIncludeRegex设置清楚再在上下文中发起扫描。主动扫描通常很慢因为ZAP会对每个参数、每个请求灌入大量攻击载荷。如果压测目标有成百上千个URL一次完整主动扫描可能要几小时。所以在压测流程里我一般建议先做被动扫描让ZAP在k6压测过程中同步收集信息压测结束后再针对重点接口跑定向的主动扫描。3.4 文件上传场景怎么用ZAP测才不走过场标题的热词里有个印象深刻的关键词是OWASP ZAP文件上传因为文件上传向来是漏洞重灾区。存储型XSS、恶意文件执行、路径穿越、绕过上传校验这类问题在OWASP Top 10里反复出现。用ZAP测文件上传不能只丢一个空的文本文件上去那样ZAP的扫描器无法触发真正的业务逻辑。我的做法是先通过k6脚本把一个真实的文件上传请求完整打在ZAP代理上让ZAP记录下这个multipart请求的完整结构。ZAP在记录后会把这个multipart请求当作一个可扫描的fuzzing点允许扫描器更改filename、content-type、file content等字段。然后在ZAP的API里可以指定对这个请求做主动扫描ZAP会尝试各种攻击载荷来测试是否存在漏洞。还有一个细节是文件上传接口往往会限制文件大小。如果你上传的测试文件太小就不会走到实际的写盘和解析逻辑如果超过限制请求又会被拦截在上传入口。正确做法是准备几个不同大小、不同格式的样本文件并在k6脚本里通过场景控制把每个文件类型的请求都送到ZAP代理里走一遍。4. 从压测报告里捞出安全问题的信号4.1 性能数据和安全告警怎么对表看k6的测试结果会给出大量的性能数据每个请求的延迟分布、错误率、吞吐量、CPU和内存趋势。ZAP的扫描结果则会给出另一组数据风险等级、告警类型、受影响的URL。把这两组数据放在一起看的时候不能只看各自的结果而要寻找交叉点。比如k6压测报告里某个接口的响应时间在并发上升时出现了剧烈波动而ZAP在同一时间段的告警里报告了同一条URL存在高危SQL注入风险。这个交叉就不是巧合了它说明这个接口内可能存在某些资源占用极高的查询路径攻击者只要构造特定的注入payload就能让这个接口从慢变成不可用这是典型的业务逻辑层DoS风险。另一个有价值的信号是错误响应体。压测时系统资源紧张很多平时不会暴露的异常分支会被触发。如果ZAP的被动扫描在某个5xx响应里检测到了堆栈信息、SQL语句、内部路径这类数据它就是直接从压测流量中抓到的信息泄露证据。这种问题在正常情况下很难发现因为错误页面只在真出错的时候返回而真出错的时候往往又只有零散用户在访问。压测把出错频率放大几百倍ZAP再逐个响应分析信息泄露就藏不住了。4.2 把结果映射到OWASP Top 10 的模型里ZAP的告警有自己的分类体系但团队汇报时通常希望对标OWASP Top 10。我习惯在拿报告之后做一个简单的映射表方便不熟悉ZAP的人快速理解风险。ZAP告警类型映射的OWASP Top 10风险常见触发场景SQL InjectionA03:2021 - 注入登录接口、搜索接口参数未预编译Path TraversalA01:2021 - 访问控制失效文件下载接口文件名未校验XSS (Stored/Reflected)A03:2021 - 注入用户评论、富文本接口未转义Security MisconfigurationA05:2021 - 安全配置错误默认错误页面、未禁用目录列表Sensitive Data ExposureA02:2021 - 加密失效接口返回明文身份证号、手机号Upload Dangerous FileA04:2021 - 不安全设计上传接口未限制文件类型/内容Excessive Resource UsageA11:2021 - 大量资源耗尽接口未做限流、正则回溯这里的映射并不需要100%严格重要的是让团队理解ZAP的告警不是工具声明的bug而是安全风险的信号。真正怎么修复、优先级多高还需要研发结合业务上下文判断。4.3 自动生成报告并留档压测和扫描的结果如果只留在终端输出里那没有任何意义。我建议把k6的测试结果输出成JSON或者CSV把ZAP的告警结果通过API拉取并保存成HTML或JSON报告。k6的JSON输出很简单k6 run --out jsonload-test-result.json load-test.jsZAP API拉取全部告警的命令也很直接curl -X GET http://127.0.0.1:8080/JSON/core/view/alerts/ \ -H Accept: application/json zap-alerts.json有了这两个文件后续无论是做趋势分析还是在项目周报里展示问题数量变化都能直接拿数据说话。我还可以用一个简单的脚本把两者合并成一张概要表列出每个压测场景下的告警数量和风险等级分布这在团队里做安全质量门禁时非常好用。5. 优化策略把压测安全集成做成长期可复用的事5.1 别让压测环境成为瓶颈很多团队第一次跑通k6和ZAP的集成以后会觉得效果不错然后开始天天跑。不好意思如果你没有把环境隔离做好很快就会发现压测结果一天一个样而且全是无效数据。压测环境必须保持稳定——数据库数据量不能忽大忽小缓存预热状态要可控上下游依赖要么mock掉要么固定版本。ZAP的扫描结果也一样如果你对测试目标之外的东西发起了扫描甚至把扫描流量打到了生产环境那结果不仅没价值还可能引发故障。我在做环境准备时有一套固定流程指定专门的压测服务实例单独准备一套压测数据库使用固定的测试账号和测试文件。每一次压测开始前先跑一遍小的冒烟脚本确认服务版本和环境状态再做正式压测。流程虽然繁琐但保证了生成报告的横向可比性这比自动化本身更重要。5.2 把k6和ZAP集成塞进CI流水线如果压测安全集成只存在于电脑里那它的价值就局限于你自己这台终端。最理想的状态是把这套流程变成流水线的一部分。k6提供了官方的Docker镜像ZAP也有Docker容器二者配合docker-compose或者k8s Job就能很方便地接入CI。一个常见的流水线做法是代码合并后在测试环境部署一套最新的服务然后启动ZAP容器接下来运行k6压测压测过程中自动将流量通过ZAP代理压测结束后调用ZAP API触发主动扫描最后汇总k6阈值结果和ZAP告警等级如果任何一个高危指标失败则阻塞流水线。在这个流程里最容易被忽略的是k6的退出码和ZAP的告警阈值如何合并判断。k6有自己的thresholds退出码非0就代表压测失败。ZAP没有现成的退出码但你可以写一个简单的脚本判断告警JSON里是否存在High风险告警有则退出码置1。同一份流水线脚本把这两个状态合并起来作为整个stage的结果输出。5.3 补一个限量提醒扫描器的成本也是成本ZAP的主动扫描非常消耗资源扫描一个复杂的Web应用可能需要几小时而且会发起大量并发请求。如果这套流程每次代码合并都跑一遍完整主动扫描你的CI机器迟早要报警。优化策略是把主动扫描的频率降下来常规的每次合并只跑被动扫描主动扫描放在每天定时任务里跑或者只针对重点变更的模块跑定向扫描。还有一个思路是给ZAP配置上下文和扫描范围把不需要扫描的静态资源、第三方脚本、健康检查接口排除掉。这样既能加快扫描速度又能减少误报。ZAP右上角的上下文管理页面可以很细致地设置包含和排除的正则规则我第一次把这些规则配好之后扫描时间直接缩短了60%以上。6. 常见问题与排查技巧实录现象可能原因排查与解决k6请求走ZAP代理后大量证书报错被测系统为HTTPSZAP的CA证书未受信在目标服务所在环境中安装ZAP CA证书或在k6脚本中配置insecureSkipTLSVerifyZAP被动扫描没有抓到任何流量k6代理配置没生效流量直接绕过ZAP检查k6的proxies配置和K6_HTTP_PROXY环境变量确认ZAP的代理端口与配置一致主动扫描迟迟不结束扫描范围包含太多URL和参数或上下文配置未生效在ZAP上下文里配置include/exclude规则通过API查看扫描进度扫描状态压测结果不错但ZAP告警极多ZAP把大量静态资源和第三方库纳入扫描调整上下文排除规则把外部CDN、JS库、静态图片文件排除k6压测导致ZAP自身崩溃ZAP内存溢出默认堆内存太小启动容器时设置JVM内存参数例如-Xmx2g或提升Docker容器资源限制断言错误率很高但业务无异常压测数据中存在的测试账号被锁或token过期检查参数化数据确保测试账号、session、token的生命周期管理正常上面表格里最值得注意的是第一条HTTPS证书问题几乎每个第一次做k6ZAP集成的人都会遇到。ZAP作为中间人代理必须能够解密HTTPS流量才能分析内容。解决方案其实不复杂但一定要做到位把ZAP生成的CA证书导入到被测服务所在机器或容器的受信任根证书库中。这里的坑在于如果你压测的是一个Kubernetes集群内部的Service证书安装位置就不是入口网关而是Pod运行环境。这个细节不处理后面所有的被动扫描结果都是空的。还有一个我在文件上传压测场景中经常遇到的问题经常被忽略那就是multipart请求的字段名和文件名必须完全匹配被测服务的预期。有一次ZAP在被动扫描时对上传的multipart请求报了一个参数缺失告警研发排查了半天发现是ZAP把filename参数重新编码了而后端框架用的是严格匹配的解析组件。这种情况下你需要确保ZAP的上下文配置里保留了multipart请求的原始参数名。遇到这类问题最好的排查方式是用ZAP的手动请求编辑器对比一下原始请求和ZAP篡改后的请求差异然后调整上下文规则。另一个容易被误解的现象是ZAP报告里出现大量中风险告警比如Missing Anti-clickjacking Header或者Cookie Without SameSite Attribute这类告警并不一定代表系统存在可利用的漏洞。很多内部系统对这类问题有明确的业务决策比如它本来就是一个内部工具不涉及第三方页面嵌套。所以ZAP的结果一定要有人工评估环节不能设置成只要识别到中风险告警就自动阻断发布不然你的流水线天天红色团队很快就会麻木反而把真正的高危风险忽略了。我在做流程的时候通常把High和Critical风险设置为强制拦截中风险只记录并推送通知留到人工评估时再判断。最后说一说我自己跑了差不多两个季度k6ZAP集成之后的体会。最值钱的不是那套自动化脚本也不是那几份ZAP报告而是团队开始形成一种新的讨论习惯。压测数据出来之后不再只是看吞吐量和响应时间也会看一眼安全告警数量有没有随负载上升而增加。研发在改代码时也会主动问一句这个接口如果被大量调用会不会有什么安全隐患。这种习惯的形成靠一次宣讲是不够的需要一套能持续运转的集成流程不断输出结果作为支撑。如果你现在正打算搭建这套体系我建议你先从最小可行版本开始一个k6脚本、一个ZAP容器、一份合并报告跑通之后再慢慢加场景、加规则、加流水线。与其追求一开始就把所有环节设计完美不如让它先转起来。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →