基于机器学习的Web攻击检测系统:从特征工程到模型落地的完整指南
发布时间:2026/9/26 17:06:55来源:尧图网络
简介面向Web安全与机器学习交叉方向的开发者该资源包以XSS与SQL注入攻击检测为目标提供两套WAF完整实现AiWaf-1基于聚类思路AiWaf-2则覆盖GRU、CNN、KNN、SVM、RF五个模型流程包含数据加载、URL解码与小写化预处理、Word2Vec向量化补齐、训练、预测及评估适合从零复现。包内共66个文件以Python源码17个py、编译产物15个pyc、说明文档txt/md、数据与模型文件csv/pkl/pickle/pb/h5/word2vec、图片png/jpg以及抓包样本pcap为主整体约26.61MB目录中AiWaf-1、AiWaf-2各自独立安排code、data、model、images便于按模块对照学习。README.md和Readme.txt分别说明了两个子项目的环境依赖、运行入口与基本配置结合csv数据及pcap抓包样本可快速验证检测效果。已有301人学习浏览对开展机器学习WAF研究、安全竞赛或相关课程设计具有直接参考价值借助源码、文档及序列化模型读者可理解数据处理与检测流程并在此基础上继续改进和扩展。1. 基于机器学习的web攻击检测系统源码包先搞懂它在解决什么问题一套基于机器学习的web攻击检测系统源码摆在眼前很多人第一反应是跑一下train.py看准确率刷到多少。真正落地时才会发现难点根本不在模型数据长什么样、特征怎么对齐、线上延迟扛不扛得住、误报会不会把运维群炸掉每一项都比调参更磨人。这类项目包通常包含HTTP日志数据集、特征提取脚本、模型训练代码和一份说明文档目标是让你从原始请求一路走到一个能读分、能出告警的检测接口。适合三种人做毕设想找完整题目的学生内网想加一道检测层的运维以及想评估机器学习检测值不值得上的安全从业者。下面按实际落地的顺序拆开讲。2. 数据与特征工程把HTTP请求变成模型能读的向量2.1 数据从哪里来公开数据集起步再换真实日志先解决训练数据的问题。这类web攻击检测源码里最常见的配套数据是CSIC 2010格式的HTTP请求日志里面包含了大量正常请求和SQL注入、XSS、路径穿越等攻击样本。CSIC是学术数据集流量分布和真实业务差得很远但优点是类别干净、样本量大适合先把整条管线跑通。我的习惯是先用它验证特征提取和模型训练代码确认整个脚本链路没有写错再把自己的nginx访问日志或网关日志灌进去替换数据源。源码包的文档说明部分一般也会注明“替换为真实业务日志后需要重新训练”这句话不是客套是必须做的步骤否则模型到了公网流量上会全面失控。拿到真实日志后标签从哪来常见做法是分成三类WAF曾拦截的请求标为攻击用工具复现出的攻击payload标为攻击普通用户请求标为正常。这里存在一个隐蔽问题WAF拦截日志本身就是“被规则识别出的攻击”拿它训练出来的模型学到的分布也是规则覆盖的那部分天然学不出能绕过规则的未知攻击。所以如果目标是未知变种检测还需要从真实流量里随机抽样人工标注哪怕只标几百条带回来的收益也远比多调一轮参高。数据清洗必须在训练前做。日志里常见的扫描器、监控探活、压测脚本请求量大且特征明显会让模型把“UA里带了监控系统名”这种与攻击无关的信息当成强特征。我一般会过滤掉User-Agent为空的请求、请求行非标准的行把明显爬虫和监控流量单独打成一个类别或直接剔除。清理规则一定要落成代码不能手工在Excel里筛否则下次换一批日志又得重新处理。2.2 两类特征设计统计特征抓习惯文本特征抓payload特征工程在整套系统里属于性价比最高、也最容易做砸的部分。我生产项目里常用的特征线索是两条请求统计特征和载荷文本特征。统计特征描述的是“这条请求长什么样”请求路径长度、query长度、body长度、参数个数、URL编码字符数量、特殊字符占比、数字与字母比例。它们抓的是攻击行为的共性。注入payload通常会拉长请求、增多参数、抬升特殊字符浓度目录扫描则表现为路径深度固定、query很短、参数很少。统计特征的主要作用是兜住大部分扫描器和自动化脚本这一类流量占检测告警的大头。文本载荷特征针对query和body本身。做法是把URI和body拼成一个字符串做字符级n-gram后再进TF-IDF。为什么不直接用词级因为真实流量里的攻击payload基本都变过体大小写混写、中间塞注释、URL编码、多余空白任何一种简单变换都能让“union select”这个单词特征失效。字符级n-gram则完全不同“unio”“n se”“elec”这类局部片段即便被噪声打散也依然能稳定出现对SQL注入和XSS的识别远好于词级。代价是特征维度高要靠max_features和min_df限制规模。两类特征最终会拼接成一个向量。常见比例是统计特征三十到五十维文本TF-IDF一千到三千维合并后送入分类器。文本维度不能贪多我见过有人把ngram_range开到(1,5)、max_features设成两万训练时间翻了几倍验证集效果却一路过拟合。建议先用(2,3)或(1,3)起步等PR曲线出现平原后再考虑放长n-gram。2.3 特征工程脚本把一条原始请求变成特征向量下面这段代码把method、uri、body转成一组适合入模的统计特征是训练和线上共用的核心模块。import re import urllib.parse def extract_basic_features(method: str, uri: str, body: str ) - dict: 把一条HTTP请求的method/uri/body转成统计特征字典 feat {} # 拆分path与query if ? in uri: path, query uri.split(?, 1) else: path, query uri, params urllib.parse.parse_qsl(query, keep_blank_valuesTrue) feat[method_get] 1 if method.upper() GET else 0 feat[path_len] len(path) feat[query_len] len(query) feat[param_count] len(params) feat[body_len] len(body) # 编码与关键词特征 raw uri body low raw.lower() feat[url_encoded] len(re.findall(r%[0-9a-f]{2}, low)) feat[sql_keyword] 1 if re.search(runion\sselect|sleep\s*\(|insert\sinto, low) else 0 feat[script_keyword] 1 if re.search(rscript|onerror|alert\s*\(, low) else 0 # 特殊字符占比刻画注入类动作 special_chars sum(ch in \;()%! for ch in raw) feat[special_ratio] round(special_chars / max(len(raw), 1), 4) return feat这段代码的逻辑很直接先解析URI里的query参数把路径长度和参数数量拆开然后通过正则统计URL编码次数和危险关键词。method只保留GET标志因为攻击请求在method维度并不存在稳定规律保留四个method的独热编码反而容易过拟合。url_encoded特征用来抓编码绕过型payloadsql_keyword和script_keyword是规则型特征它们只是特征不是拦截规则模型会自己学权重不会因为某个词出现就一刀切。参数方面re.findall里的%[0-9a-f]{2}只匹配两位十六进制编码遇到UTF-8多字节编码会被拆成多次匹配这是预期行为。query使用parse_qsl解析参数顺序会保留如果业务里有嵌套JSON参数这个函数解不出需要在后面加一层JSON解析逻辑。special_ratio的分母用max(len(raw), 1)避免空请求除零保留四位小数防止数值过大干扰逻辑回归收敛。文本特征部分单独用一个TfidfVectorizer处理与统计特征并行from sklearn.feature_extraction.text import TfidfVectorizer text_vec TfidfVectorizer( analyzerchar_wb, ngram_range(2, 3), max_features2000, min_df3, sublinear_tfTrue, )analyzerchar_wb表示在词边界内做字符n-gram对URL这种没有空格分词的字符串非常稳。ngram_range(2,3)在日志量几万条时是性价比很高的区间max_features2000防止文本维度爆炸配合min_df3把只出现一两次的噪声片段滤掉。sublinear_tfTrue用1log(tf)压缩词频避免高频字母“e”“a”这类无意义片段主导特征距离。这套参数直接复用到训练Pipeline里即可。3. 模型选型与训练评估从基线模型到调阈值3.1 先跑基线逻辑回归和随机森林已经够用不建议跳过传统模型直接上CNN或LSTM。字符级深度模型在文本分类上确实效果好但代价是样本量至少要几十万条起步且训练和推理都要GPU或优化框架。在一个常规的web攻击检测项目里样本量通常只有几万条这时随机森林和逻辑回归往往不比深度模型差而且有不可替代的优点可解释。逻辑回归给出的权重可以直接打出来看哪些特征在把请求往攻击方向推一目了然。随机森林则可以输出feature_importance用来做特征筛选。这两个模型在几千到几万请求规模下通常能把攻击类recall做到0.95以上F1到0.9附近够用的前提下没必要把工程复杂度抬上去。模型选型对比起来也清晰模型适用规模主要优势主要问题逻辑回归千到百万级训练快、权重可解释、调参少对强非线性特征组合表达弱随机森林千到十万级能抓非线性关系、特征重要性可直接用特征维度高时训练慢、内存占用大XGBoost/LightGBM万级以上精度上限高、支持缺失值超参数多容易在小样本上过拟合字符级CNN/LSTM十万级以上自动提取局部文本模式训练慢、推理延迟高、解释困难常见做法是先跑逻辑回归建立baseline再用随机森林对比最后根据验证集表现决定要不要上梯度提升树。深度模型放到项目后期当优化项而不是第一步就扎进去。3.2 评估指标准确率是陷阱召回率和误报率才是关键在安全检测里准确率是一个非常误导人的指标。设想正常请求占99%、攻击占1%模型什么都不做直接全判正常准确率就是99%看起来性能完美实际上一个攻击都没拦住。正确的评估方式是看两类各自的precision和recall外加AUC。攻击类recall是漏报率的最好体现recall 0.95意味着每100个攻击样本能拦下95个剩下5个会漏过去。正常类precision反映误报水平误报率太高会直接消耗运维信任最后变成狼来了。实际调优时我习惯把攻击类的recall目标定在0.95以上同时控制正常类的误报率在0.1%以内如果两者冲突优先保攻击类recall因为漏报的代价远高于多告警一次。3.3 训练评估代码Pipeline整合特征与模型下面是一个标准的训练脚本骨架把数值缩放和分类器放进同一个Pipeline避免上线时漏掉某一步预处理。import joblib import pandas as pd from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.metrics import classification_report, roc_auc_score # df 是已经完成特征提取的DataFramelabel1表示攻击 X df.drop(columns[label, raw_uri]) y df[label] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) pipeline Pipeline([ (scale, StandardScaler()), (clf, LogisticRegression(C1.0, class_weightbalanced, max_iter1000)), ]) pipeline.fit(X_train, y_train) y_pred pipeline.predict(X_test) y_prob pipeline.predict_proba(X_test)[:, 1] print(classification_report(y_test, y_pred, target_names[normal, attack])) print(AUC:, roc_auc_score(y_test, y_prob)) joblib.dump(pipeline, model_lr.joblib)train_test_split里stratifyy保证划分后正负样本比例与原始一致random_state42固定随机种子让每次实验可比。Pipeline里先做StandardScaler再做逻辑回归对基于距离和梯度的模型至关重要文本TF-IDF维度尤其需要这一步。class_weightbalanced自动把少数类权重放大缓解正常请求与攻击请求数量悬殊的问题。max_iter1000确保lbfgs收敛不会被警告打断。AUC指标对类别不平衡不敏感适合作为模型排名的第一筛。换成随机森林只需要改Pipeline里的分类器并给n_estimators和max_depth一组保守值from sklearn.ensemble import RandomForestClassifier pipeline Pipeline([ (clf, RandomForestClassifier( n_estimators200, max_depth10, min_samples_leaf5, class_weightbalanced, random_state42, n_jobs-1, )), ])随机森林不需要特征缩放所以Pipeline里省去了StandardScaler。n_estimators200是精度和训练时间的折中点超过200收益很小max_depth10限制单棵树深度防止小样本过拟合min_samples_leaf5确保叶子节点至少有5个样本能明显降低线上误报抖动。n_jobs-1用满CPU核心训练速度快很多。跑完后对比两个模型的AUC和攻击类recall选高的那个继续后面阈值校准。4. 系统落地离线模型如何接到在线流量上4.1 落地架构日志采集、特征管道、模型服务模型训练完只是第一步把它变成能对线上请求打分的服务才是真正的工程活。常见架构是nginx访问日志或流量镜像进入消息队列消费端做日志解析和特征提取再调用模型服务打分最后把分数写给ES或数据库由告警模块决定是否通知。如果做同步拦截模型推理要嵌在网关或WAF的请求链路里延迟预算很紧如果是旁路检测可以接受秒级延迟用异步队列更稳妥。我一般建议先做旁路。直接把模型嵌进网关的风险是一旦推理接口抖动整个业务请求都会被拖死。旁路方案里日志照常进模型打分异常只影响检测不伤业务。等模型稳定运行一两周误报率验证通过了再考虑把打分结果回传给网关用于触发二次校验或阻断。4.2 模型上线用joblib和Flask封装检测接口训练时保存的joblib模型文件只包含分类器本身特征提取逻辑仍然在代码里。上线最关键的一点是线上推理必须复用训练时的同一份特征代码禁止在接口里重写一套特征逻辑。下面用一个Flask接口示例。import joblib import pandas as pd from flask import Flask, request, jsonify from feature_extract import extract_basic_features app Flask(__name__) model joblib.load(model_lr.joblib) app.route(/detect, methods[POST]) def detect(): data request.get_json(forceTrue) method data.get(method, GET) uri data.get(uri, /) body data.get(body, ) feat extract_basic_features(method, uri, body) prob model.predict_proba(pd.DataFrame([feat]))[0][1] return jsonify({score: round(prob, 4), attack: int(prob 0.7)}) if __name__ __main__: app.run(host0.0.0.0, port8000)这个接口只做三件事解析JSON请求体、调用同一个特征函数、返回攻击概率。feature_extract模块在训练脚本里被import过在这里又被import一次保证训练和推理看到的特征完全一致。pd.DataFrame([feat])把特征字典包成一行DataFrame列顺序就是字典插入顺序Python 3.7以上dict保序所以只要特征函数没改列顺序就不会变。返回结果里没有直接给label而是给了score和attack两个字段。score是连续概率上层可以按业务敏感度动态调阈值不用重新部署模型。attack字段里的0.7只是初始值后面按验证集校准。4.3 阈值调整0.5只是起点要按验证集重新校准模型默认的0.5阈值几乎从来不是最优选择。在类别不平衡明显的数据上0.5会把大量模棱两可的请求推到攻击侧误报直接起飞。正确做法是拿验证集算每一档阈值下的攻击类recall和正常类误报率选一个业务能接受的平衡点。具体操作对验证集所有样本调用predict_proba得到攻击概率后从0.1到0.9按步长0.05扫描计算每个阈值下recall和fpr画一条曲线。如果业务要求攻击类recall不低于0.95就在满足这个约束的阈值里挑fpr最低的那个。误报率限制通常来自运维的容忍度比如每万次正常请求最多误报5次对应fpr为0.0005。这样选出来的阈值可能是0.3也可能是0.8完全取决于数据分布不能拍脑袋定。阈值确定后动作也要分级。我习惯把高于阻断阈值的请求标记为“block”介于告警阈值和阻断阈值之间的标记为“watch”只记录不拦截。机器学习检测系统上线初期宁可多记几个watch也不要急着把模型输出直接接到防火墙。逐步放开比一把梭安全得多。5. 避坑指南脏数据、误报与性能问题的排查5.1 现象测试集F1很高一上线全是误报现象模型在验证集上F1刷到0.95部署到线上后正常请求大量被标成攻击告警刷屏。原因最常见的是数据泄漏。训练脚本用train_test_split随机切分数据时同一个用户会话的多条请求同时出现在训练集和测试集里模型直接“记住”了URL和参数模式而不是学到攻击行为的本质。真实日志里同一个IP的请求是连续成片的随机切分必然导致这种泄漏。解决按时间窗口切分或者按IP/会话ID分组再切分保证同源请求不会跨数据集。更稳妥的办法是上线后先跑48小时影子流量把线上真实样本收集起来与测试集对比看概率分布是否一致。如果线上打分的均值明显偏高说明特征分布已经漂移了模型需要重新评估。5.2 现象明显攻击请求模型不识别现象手工构造的union select、 这类请求模型打出的分数只有0.2漏报得莫名其妙。原因训练数据里攻击样本太少或者样本都以编码形态存在特征提取阶段没有做解码归一。比如query里携带%75nion%20selecturl_encoded特征会捕获编码密度但TfidfVectorizer处理的是原始字符串字符级n-gram被%和数字打散模型根本看不到完整的攻击片段。解决在特征提取前先对query做一次urllib.parse.unquote_plus解码再进行n-gram和关键词匹配对中文参数值做好UTF-8解码避免乱码进特征。攻击样本也要做扩充把已有的payload改成大小写混写、加注释、加空白三种变体样本量不够时这种数据增强非常有效。5.3 现象加了文本特征后训练慢、内存飙升现象只跑统计特征时训练几十秒把文本特征接进来后内存直接占满随机森林训练十分钟没结束。原因TfidfVectorizer的n-gram_range设得过长max_features没有限制导致特征维度爆炸。ngram_range(1,5)会让4个字符以上片段数量指数级增加几万条请求能造出上百万维特征随机森林在这种维度下训练和内存开销都扛不住。解决ngram_range先固定(2,3)max_features限制在2000到3000min_df3滤低频。先跑通数值特征管道再加文本特征对比效果。维度实在不够用时优先考虑哈希向量化HashingVectorizer而不是无限放max_features它能固定特征维度且无需维护词表。5.4 现象线上接口响应慢顶不住流量现象检测接口单次请求耗时40到60毫秒压测时每秒500个请求就开始堆积网关链路被拖慢。原因Python环境的正则解析、字符串拼接、TF-IDF向量化每一步都有不小的开销每次请求都重新做一遍特征提取和模型推理完全没有复用。解决最立竿见影的做法是给特征加缓存同一uribody组合直接复用上一次的得分正常流量中重复请求占比很高能砍掉一大半计算量。模型推理部分如果延迟还高把sklearn模型导出成ONNX格式用ONNX Runtime跑推理时间通常能降到原来的三分之一。再不够就改成异步队列日志先进队列模型打分消费端自己排队处理不再阻塞主业务链路。5.5 现象模型被变形payload绕过现象换一种不会出现在训练集里的编码方式或者把payload拆成多段拼接模型就识别不出来了。原因机器学习模型学到的是训练集的统计分布不是安全专家的语义规则。攻击者只要把特征分布挪出训练覆盖范围比如把select拆成sel||ect、把关键字换成全角字符模型就失去了判断依据。模型没有万能解。解决别把模型当唯一防线。我的做法是保留一组精确规则union select、
网站建设高端定制企业官网