fuzz-hardening 用于对读取不可信数据的代码持续投喂生成式敌意输入,直到代码能正确处理,或由此暴露缺陷。使用者即使不熟悉模糊测试也可使用:技能自行做技术判断,每项判断用一句话说明,只在答案会影响工作内容时提问。
预设决策 除非项目打破前提假设,否则不重新讨论以下选择:
- 技术:使用 fast-check(fast-check + @fast-check/vitest,作为开发依赖)做基于属性的模糊测试。JavaScript 内存安全,缺陷多为未处理异常、500 错误、错误结果、NaN、挂起,fast-check 能发现并将失败输入收缩为最小用例。
- 运行器:沿用现有 Vitest 配置,文件命名为 *.fuzz.test.ts 并放在被测代码旁,由项目测试脚本自动拾取,vitest run .fuzz. 只跑模糊测试文件。
- 层级:直接调用函数、Zod schema 或处理器,不走 HTTP,每秒可运行上千次,无需服务器,结果确定。
- 运行次数:每次测试运行 numRuns: 100;test-fuzz 脚本使用 FUZZ_RUNS=20000。PR CI 保持快速,深度运行按需开启。
- 不使用:Jazzer.js、libFuzzer、AFL(覆盖率引导、Node 原生、非 Vitest/Bun);Schemathesis 等 HTTP 模糊器(FMC 应用没有 OpenAPI 规范)。对该技术栈成本高于价值。
测试装置代码、生成器与不变量目录见 references/harness-patterns.md,在步骤 3 加载。
工作流程
-
预检:查看技术栈、测试配置、已有模糊测试文件。
-
梳理攻击面,形成排序后的目标清单。
-
为选定目标编写测试装置(每次不超过 5 个)。
-
运行、判定、修复、固化回归。
-
接入脚本与 CI。
-
报告。
-
预检 读取 package.json(脚本,预期含 test-run / check / check-ci;依赖 vitest、fast-check、zod、@prisma/client、@turf/*)、Vitest 配置(testTimeout、@/ 等路径别名)以及 .github/workflows/。检查是否已有 *.fuzz.test.ts,若存在则视为后续运行,扩展而非重复。 若没有 Vitest,停止并告知用户;可提议添加(一个开发依赖 + vitest.config.ts + test-run 脚本),不要悄悄回退到 bun test。在 monorepo(app/、processing/)中,按拥有独立 package.json 的包分别处理。
-
梳理攻击面 从成本最低处开始搜索,跳过只做 UI 渲染或仅封装第三方库且无自有逻辑的部分。重点关注:createServerFn(...).validator(...) 与 API 路由(请求体由攻击者控制);routerSearch.parse / .stringify 与 validateSearch(URL 可被任意编辑,解析错误破坏页面,往返缺陷破坏链接);含 .transform、.refine、.preprocess 或自定义强制的 Zod schema(校验内部的自定义代码可能接受后续代码无法处理的值);GeoJSON、坐标、bbox 解析,对用户几何调用 Turf,WASM 地理函数(非法环、反经线、NaN、空几何);上传与导入解析(CSV、GeoJSON、OSM 标签、对请求或文件数据做 JSON.parse);$queryRawUnsafe、$executeRawUnsafe、Prisma.raw、拼接 SQL、ST_GeomFromGeoJSON(注入与 PostGIS 错误变成 500);自有纯工具(slug、normalize、日期格式化/解析、单位换算,成本低的不变量如往返、幂等,以及夏令时边界)。对用户数据做字符串插值的原生 SQL 本身即为发现项,应改用参数($queryRaw 标签模板 / Prisma.sql)修复,而不是对它做模糊测试。 排序依据:不可信程度 × 自有逻辑 × 影响范围。服务端输入 > URL 输入 > 文件导入 > 内部工具。 用一次提问(多选)给出前 3–6 个目标与推荐项,选项面向从未听说过模糊测试的人:它是什么、谁能向它发送输入、我们将检查什么,不用不变量名称或生成器术语。若目标不超过 3 个且均明显属于服务端输入,可跳过提问直接说明清单并继续。
-
编写测试装置 一次性安装:bun add -d fast-check @fast-check/vitest。按项目导入风格(别名或相对路径)添加共享运行次数辅助函数。每个目标只测试目录中契合的一到两个不变量,不要全部测试。每个装置配两个生成器:合法形状输入(按 schema 字段构造,使 schema 大多接受,用于发现校验之后的逻辑缺陷)与恶意输入(fc.anything()、损坏 JSON、错误类型,必须被干净拒绝)。合法形状生成器应贴近 schema 实际接受的范围;fc.pre(...) 只用于跳过个别情况,若 schema 拒绝大多数生成值,fast-check 会以“too many pre-condition failures”中止,此时应修正生成器而非调高上限。若目标需要 Postgres/PostGIS(原生 SQL、对已存几何执行 ST_*),先检查是否已有测试数据库配置(Docker compose 服务、测试环境 DATABASE_URL);没有则先询问用户是否添加,期间只模糊测试纯逻辑部分(将到达数据库的输入),跳过数据库步骤。
-
运行、判定、修复 使用项目自有 Vitest,而非 bunx:先以默认运行次数执行 bun vitest run .fuzz.,再执行一次 FUZZ_RUNS=5000 bun vitest run .fuzz. --testTimeout=120000。两种运行次数均通过即视为该目标完成,报告为已覆盖并继续;不要为了强行找出问题而增加不变量或提高运行次数。对每个失败,fast-check 会打印收缩后的反例以及种子和路径,据此判定:恶意输入导致崩溃、未处理异常或 500 属缺陷,在边界修复(收紧 schema、捕获、返回 4xx),无需提问;schema 接受但下游抛错或返回 NaN/Infinity 属缺陷,收紧 schema 或加固函数,无需提问;往返不匹配或意图明确的错误结果属缺陷,修复;结果“错误”但意图不明(如接受空名称、-0、孤立代理项、德国以外坐标),用具体输入、当前输出和两种可能规则提问,并合并为一次提问;装置本身有误(生成器产生不可能输入、预条件失败过多),修正生成器而非代码,并在报告中说明;仅在高 FUZZ_RUNS 下出现 Vitest 超时,不是缺陷,属性较慢而非卡住,可提高 --testTimeout 或降低该装置的 numRuns,真正的挂起在 100 次运行下用单个输入即可复现。每次修复后,将收缩后的输入固化为同文件中的普通 it(...) 回归测试;绝不削弱不变量以让运行通过。设置会话上限:若某目标经过三轮修复仍不断产生新失败,停止、列出问题并询问是否继续该目标。
-
接入脚本与 CI package.json 添加 "test-fuzz": "FUZZ_RUNS=20000 vitest run .fuzz. --testTimeout=600000"(kebab-case,不用冒号);不要改动 check / check-ci,因为模糊测试文件已在 test-run 中以 100 次运行执行。CI 方面,现有 PR 工作流运行 check-ci → test-run,无需新增;可询问(默认是)增加每周定时任务运行 bun run test-fuzz,模板见参考文档。定时运行失败会打印种子,可据此本地重跑。
-
报告 用面向模糊测试新手的简明语言说明:覆盖的目标及每个目标的不变量、执行次数;发现并修复的缺陷(每条一行,附收缩输入);待解决问题与跳过的目标及原因;如何重跑(bun run test-fuzz)以及如何重放 CI 失败(种子/路径)。随后交给 finish-work 技能做检查与提交(主题通常为 Dev,或在修复了用户可见缺陷时为受影响领域)。
范围之外 本技能只聚焦输入处理。运行期间不涉及:React 组件/UI 模糊测试(流程用 playwright-skill);依赖漏洞(review-dependabot);GitHub Actions 或 Dockerfile 加固、认证与权限审查(若发现严重问题,作为独立工作告知用户,不在此修复)。
相关技能 tech-stack、tanstack-start-conventions(服务端函数、API 路由校验)、tanstack-router-conventions(搜索参数序列化)、rust-wasm-geo(WASM 与 Turf 差分)、prisma、finish-work。