03-把机器学习塞进 WAF,最难的不是模型

把机器学习塞进 WAF,最难的不是模型

ShieldWAF 系列文章第三篇,2026-10。讲智能防护(我们内部叫 ML 模块):特征契约怎么保证两端一致、训练闭环怎么自动化、上线为什么刻意"慢半拍",以及集成阶段踩过的坑。

对标雷池(长亭)白皮书的那轮差距分析里,结论最重要的一条不是"我们缺一个语义引擎",而是"我们缺一条从流量到模型的完整闭环"。规则引擎是天生的残缺猎手:CRS 三百多条规则写得再好,也只认识已经写下来的攻击。攻击者换个编码、换个变体,规则就要等下一次更新。

所以今年我们做了三件事:给引擎加语义分析器(13 种结构突变信号),把语义信号做进特征契约喂给模型,然后把训练、评估、灰度、回滚整条链路自动化。模型本身反而是最简单的部分——六个算法(随机森林、孤立森林、自编码器、XGBoost、LSTM、Transformer),按任务类型挑就行,训练器依赖锁定到具体版本保证可复现。真正吃掉工作量的是三件事:特征契约、训练闭环、上线纪律。

特征契约:两个进程、两种语言,怎么保证看到同一个世界

WAF 的 ML 和推荐系统有个本质区别:训练侧是 Python,推理侧是 Go 引擎,特征工程没法共享一份代码。

最省事的做法是训练侧独立实现一份特征抽取。我们一开始就没走这条路,因为它必然漂移——Python 版改了一行、Go 版没跟上,模型就在一个训练时从未见过的特征空间上打分,而且没有任何报错。

我们的做法:特征抽取只在引擎侧实现(feature_extractor.go),训练器从导出的样本里用同源算法复算,两边对着 golden 用例逐维对拍(compare_golden.py),worst_diff 9.5e-8——浮点求和顺序造成的,可接受。契约带版本号,追加式演进:

v1    0-39    行为/统计/关键词特征(40 维)
v2.2  40-45   TLS 指纹与上传特征(追加)
v3    46-51   语义特征族:sem_sql_quote / sem_sql_union /
              sem_sql_bool / sem_js_inj / sem_cmd_inj / sem_score

追加式不是图省事,是刻意的兼容策略:旧模型的维度可能是 40 或 46,引擎按模型自己的维度做前缀切片,旧模型不重训就能继续跑,新模型训练后自然消费语义槽位。换句话说,运行时永远不存在"新增特征"这回事——训练和推理看到的向量,契约在发布前就锁死了。

序列特征最容易出阴间 bug,所以窗口语义写得极其死板:8 步窗口、oldest→newest 排列、步数不足在 oldest 侧补零、最新请求恒在末位(LSTM 读最后时间步)。训练导出(ExportSequences)和引擎的 IP 环形缓冲(ipFeatureRing)照着同一段契约文档各写一遍,谁也不许自由发挥。

特征掩码是契约的另一半。某个特征上线后发现带毒——比如会把训练集污染掉——不能直接删,删了维度就变了,旧模型全废。做法是掩码随训练任务快照进配置,训练器在标准化之后置零并写进产物 feature_spec.mask,引擎用 applyMask 按同样的顺序置零。训练和推理看到的还是同一个(被打码的)向量。

训练闭环:让机器盯着机器

整条链路是自动化的:

控制面样本库 → 自动导出(≤2 万条按比例截断)→ 训练器
→ 训练 → held-out 评估 → 门禁 → ONNX 导出
→ 回调 /v1/sync/ml-models 入库 → checksum 级联 → 引擎 5s 内重拉生效

三个设计点。

门禁不过就 409。 held-out 指标不达线的模型连模型库都进不了,除非管理员显式带 ignore_gate:true 越权并留审计记录。灰度推进的每一步都有退路:每次同步包里都带着上一版的完整五件套(模型/scaler/掩码等)作为 canary 备份,canary 有问题自动回退 active,主动回滚也是秒级的事。

机器通道和管理通道分开。 训练器是个无人值守的进程,没有用户凭证。早期它直接打管理端的模型上传接口——必 401。这个 P0 级的鉴权断裂是测试轮揪出来的。通道分开之后,回调走 /v1/sync/ml-models(API-Key 组,和引擎拉取同路径不同方法),但校验一点没少:checksum 强校验、序列模型的 io_spec 按 shape[-1] 对齐、scaler 不匹配一票否决。

贡献归因。 运维不看混淆矩阵,他问的是"这条为什么被拦"。引擎在打分时顺手算 topContributions(z 分 × 特征重要性,取 top-5 落库),事件详情里直接给答案。ML 系统的可信度,一半来自它能解释自己。

上线纪律:慢半拍是故意的

ML 拦截最大的风险不是漏报,是"自信地误杀"。所以引擎侧的 ML 全部默认温和:

  • 灰度:gray_ratio 1-100,按 FNV-1a(clientIP|requestID) 哈希分流,同一个请求永远由同一个版本打分,事件里记录实际打分版本——出问题能精确圈出影响面。
  • 挑战代替拦截:block 模式下,落在挑战分数区间内的 pass/monitor 请求升级成 ML 验证码挑战,只升级、不降级;区间配置非法时 fail-safe 整个关闭。
  • 高分人工确认:ML 单独驱动的拦截自动降级成 monitor,事件打上"待人工确认"标记,等人裁决;规则引擎的拦截不受影响。原则就一句:ML 永远不当终审法官。
  • 反馈闭环:误报标注带原因回流 normal 样本,漏报回流 attack 样本,漏报还计入自动重训阈值。运维的每一次标注,都在训练下一个模型。

集成的坑,占了这个模块一半的工作量

三个真实 bug,都是"模型正确、工程错误"的典型:

  1. 键型断裂。训练器 /train 收到的 job_id 是 JSON 整数,/status/{id} 回查时是 URL 字符串,内存里的任务表键型不一致——所有训练任务的状态永远查不到,调度器把它们全部判成超时。线上历史自动学习任务全失败的真因,最后就一行代码:统一 str()。
  2. 数据集不落键。控制面按契约推 by_key: {数据源key: 行},训练器把整个嵌套 dict 当行数组写进了文件,装载侧迭代 dict 键直接 'str' object has no attribute 'get'。修复是拍平成每个数据源一个文件。
  3. SQL 少个别名。评分分布接口的 UNION 第二分支 width_bucket(...) 忘了 AS bkt,接口恒 500,前端的评分分布图永远是空的。这种问题最隐蔽:页面空白,但没有任何告警——接口"成功"返回了。

还有个隐形约束:ONNX Runtime 走 CGO,本地 Windows 开发机没有 GCC,编不了带 -tags ml 的引擎,本机只能跑 stub 保底。所以训练-推理全链路的验证只能在 Linux 构建里做,CI 和生产镜像必须带上。这是还欠着的工程债。

效果

压测矩阵里专门有 ML 两档:门控开启、推理开启。实测推理增量 CPU 在噪声以下——这是敢在生产全开的底气。至于检出率,四模式(纯规则/纯 ML/融合/全关)× 检出/误报/延迟的评测矩阵跑在引擎的基准测试里(detection_matrix_test.go,每次回归都在跑),数字随语料变化,就不在这里贴了。

回头看,这个模块学到最多的一课是:ML 系统的工程质量不在模型里,在模型两头——特征怎么对齐,决策怎么克制。

Snipaste_2026-10-24_09-15-14

Snipaste_2026-10-24_09-13-52

posted @ 2026-10-24 09:19  cliffdev  阅读(3)  评论(0)    收藏  举报