☰
机器学习检测SQL注入:从流量采集到在线推理全链路实战
2026/10/2 8:32:48 网站建设 项目流程

简介:这份资源面向网络安全初学者与机器学习实践者,聚焦用分类算法区分SQL注入语句与正常语句,解决Web安全场景下的注入检测问题。包内共36个文件,以10个csv样本数据、10个Python脚本、8个已训练模型为主,另含xml配置、readme说明与iml工程文件,压缩包约1.06MB,体量轻便。数据部分提供正常与注入语句矩阵,脚本覆盖特征预处理与多算法训练,模型文件可直接加载测试。项目分别采用SVM、Adaboost、决策树、随机森林、逻辑斯蒂回归、KNN与贝叶斯等算法建模,并用准确率度量效果,便于横向对比不同分类器的表现。已有291人学习,适合想快速上手安全数据挖掘、复现经典分类流程并理解特征工程与模型评估的读者参考。

1. 机器学习检测SQL注入:从流量里把攻击揪出来

SQL注入至今仍是Web应用最致命的威胁之一。手工翻日志、靠正则匹配特征串,面对编码变形、注释混淆、盲注这些手法基本是睁眼瞎。我见过太多团队在WAF规则库里堆了几百条正则,结果一个/*!50000union*/就绕过去了。机器学习检测SQL注入的思路,是把HTTP请求当成文本序列,用模型自动学习正常请求和注入payload之间的统计差异,而不是靠人去穷举攻击特征。这个方向适合有Python基础、手头有Web访问日志或靶场流量的安全工程师和机器学习入门者。读完你能自己搭一条从数据采集、特征工程到模型训练和在线推理的完整链路,也能看清这套方案在真实业务里的边界在哪。

2. 数据从哪来:SQL注入样本的采集与标注

2.1 用靶场和日志构造正负样本

做检测模型,第一道坎不是算法,是数据。SQL注入的公开数据集不多,质量参差,我一般会自己构造。正样本(注入请求)可以从DVWA、sqli-labs、Pikachu这类靶场批量抓取,也可以用sqlmap对靶场跑一遍,把payload和完整HTTP请求存下来。负样本(正常请求)从真实业务日志里采样,或者用靶场的正常功能页面请求。关键点是:正负样本的请求结构要一致,都包含URL路径、查询参数、请求体、Header,否则模型学到的可能是“靶场请求和业务请求的差异”,而不是注入和非注入的差异。

采集时用代理抓包或直接读Nginx access log。access log默认格式信息太少,建议在Nginx配置里加上请求体和关键Header的记录,或者用mitmproxy做中间人抓取。下面这段脚本演示从sqli-labs靶场批量请求并保存原始HTTP报文:

import requests import os # sqli-labs 的注入点,Less-1 是 GET 型字符注入 base_url = "http://localhost/sqli-labs/Less-1/" # 一批典型 payload,覆盖联合注入、报错注入、布尔盲注 payloads = [ "1' AND 1=1 -- ", "1' AND 1=2 -- ", "1' UNION SELECT 1,2,3 -- ", "1' AND extractvalue(1,concat(0x7e,database())) -- ", "1' AND (SELECT COUNT(*) FROM information_schema.tables)>0 -- ", ] os.makedirs("raw_requests/positive", exist_ok=True) for i, p in enumerate(payloads): # 注意:params 会自动做 URL 编码,保留原始 payload 语义 r = requests.get(base_url, params={"id": p}, timeout=5) # 保存完整请求行、Header、Body,方便后续统一解析 raw = f"GET {r.request.url} HTTP/1.1\n" for k, v in r.request.headers.items(): raw += f"{k}: {v}\n" raw += "\n" with open(f"raw_requests/positive/{i}.txt", "w") as f: f.write(raw) print(f"saved positive {i}, status={r.status_code}")

这段代码的逻辑是:对每个payload发一次请求,把requests库重建的完整URL和Header按HTTP报文格式落盘。参数说明上,base_url换成你自己的靶场地址,payloads列表可以按注入类型扩充。注意params会自动编码,如果你要保留原始未编码的payload做特征,得手动拼URL。负样本同理,把payload换成正常参数值(如id=1、id=2)跑一遍即可。正负样本比例建议控制在1:3到1:10之间,真实场景里注入请求远少于正常请求,太平衡反而失真。

2.2 标注策略:弱标注也能用

真实业务里不可能人工标注几十万条请求。我的做法是分层标注:已知攻击IP的请求标为正,WAF拦截日志里确认误报的标为负,其余用规则引擎初筛后抽样人工复核。这样得到的标签有噪声,但配合后面会讲的鲁棒特征和模型正则化,效果够用。如果完全没标签,可以先用无监督的异常检测(如Isolation Forest)跑一版,把异常分数最高的样本拿出来人工看,逐步积累标注集。别指望一步到位,标注是个迭代过程。

3. 特征工程:把HTTP请求变成模型能吃的向量

3.1 字符级和词级特征怎么选

SQL注入检测的核心是把请求文本转成数值向量。常见做法有两类:一是字符级n-gram,把请求字符串按字符切分,统计n-gram频率或做TF-IDF;二是词级特征,按空格、等号、括号等分隔符切词,再统计。字符级对编码变形、大小写混淆更鲁棒,词级对语义结构保留更好。我一般两个都用,拼接后喂给模型。下面用scikit-learn做字符级TF-IDF:

from sklearn.feature_extraction.text import TfidfVectorizer import numpy as np # 假设已经把所有请求的 URL+Body 拼成一个字符串列表 # texts 和 labels 从上一节的 raw_requests 解析得到 texts = [...] # 每个元素是一条请求的原始字符串 labels = [...] # 1 表示注入,0 表示正常 # 字符级 n-gram,n 取 1 到 3,覆盖单字符和短序列模式 char_vec = TfidfVectorizer( analyzer="char", ngram_range=(1, 3), max_features=5000, # 控制维度,太大容易过拟合 lowercase=True, # SQL 关键字大小写不敏感,统一转小写 sublinear_tf=True # 对高频 n-gram 做对数缩放,抑制常见字符组合 ) X_char = char_vec.fit_transform(texts) # 词级特征,按空白和常见分隔符切分 word_vec = TfidfVectorizer( analyzer="word", token_pattern=r"[a-zA-Z0-9_]+|[^\sa-zA-Z0-9_]", # 保留操作符 ngram_range=(1, 2), max_features=3000, lowercase=True ) X_word = word_vec.fit_transform(texts) # 拼接两个特征矩阵 from scipy.sparse import hstack X = hstack([X_char, X_word]) print("feature shape:", X.shape)

参数上,max_features是控制过拟合的第一道闸。5000+3000是我在几万条样本上的经验值,样本少就往下调。sublinear_tf=True对TF-IDF很关键,因为SQL注入里'、-、/这些字符出现频率极高,不做缩放会主导特征。token_pattern里特意保留了非字母数字的操作符,因为'、--、/*这些正是注入的关键信号。注意fit_transform只能用在训练集,测试集必须用训练集fit好的vectorizer做transform,否则特征空间不一致,这是新手最常见的翻车点。

3.2 手工特征:那些模型不一定学得到的信号

纯TF-IDF有个问题:它只看字符组合频率,不理解“参数里出现了SQL关键字”这种语义。我一般会额外加一批手工特征,用表格列出来:

特征名计算方式为什么有用
引号数量统计'和"出现次数注入常用来闭合字符串
注释符出现是否含--、#、/**/注释用于截断原SQL
SQL关键字数匹配union/select/and/or等直接反映注入意图
参数长度查询参数总字符数注入payload通常偏长
特殊字符比例非字母数字字符占比正常请求比例低
编码次数URL编码层数多层编码是绕过手段

这些特征用pandas算好,和TF-IDF矩阵拼接。注意手工特征要做归一化(如MinMaxScaler),否则量纲差异会让树模型之外的算法跑偏。我试过只用TF-IDF和加上手工特征的对比,在逻辑回归上AUC能差3到5个点,在随机森林上差距小一些,但手工特征对可解释性帮助很大——出报警时你能告诉运维“这条请求引号数量异常且含union关键字”,而不是只给一个黑匣子分数。

4. 模型选型与训练:逻辑回归、随机森林还是深度学习

4.1 小样本优先逻辑回归和树模型

SQL注入检测的样本量通常不大,几千到几万条是常态。这个量级下,逻辑回归和随机森林往往比深度学习更稳。逻辑回归训练快、可解释、不容易过拟合,配合L2正则和类别权重能处理正负样本不平衡。随机森林对特征尺度不敏感,能捕捉非线性,但模型文件大、推理慢。我的建议是:先用逻辑回归跑基线,看AUC和召回,再上随机森林对比。下面是一个带类别权重的逻辑回归训练示例:

from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report, roc_auc_score # X 是上一节拼接好的稀疏特征矩阵,y 是标签 X_train, X_test, y_train, y_test = train_test_split( X, labels, test_size=0.2, random_state=42, stratify=labels ) # class_weight="balanced" 自动按类别频率反比加权,缓解正样本少的问题 clf = LogisticRegression( C=1.0, # 正则强度倒数,越小正则越强 class_weight="balanced", max_iter=1000, solver="liblinear" # 稀疏特征用 liblinear 比 lbfgs 快 ) clf.fit(X_train, y_train) y_pred = clf.predict(X_test) y_prob = clf.predict_proba(X_test)[:, 1] print(classification_report(y_test, y_pred)) print("AUC:", roc_auc_score(y_test, y_prob))

参数说明:C控制正则化,稀疏高维特征下我一般从1.0开始试,过拟合就降到0.1。class_weight="balanced"很重要,注入样本可能只占5%,不加权模型会倾向于全判正常,召回率惨不忍睹。solver选liblinear是因为特征维度上万且稀疏,lbfgs在这种场景下收敛慢。评估时别只看准确率,正样本少的时候准确率虚高,重点看召回率和AUC。如果召回不够,调低class_weight里正类的权重或者降低决策阈值。

4.2 深度学习什么时候值得上

当样本量到十万级以上,或者请求里存在大量需要长距离依赖的混淆模式时,可以考虑TextCNN或BiLSTM。TextCNN在字符级序列上做卷积,能捕捉局部n-gram模式,训练比LSTM快。但深度学习的坑在于:需要调参、需要GPU、推理延迟高,而且对对抗样本(比如同义词替换、插入垃圾字符)不一定比树模型鲁棒。我的一般做法是:逻辑回归/随机森林能到95%召回就先上线,深度学习作为后续优化项。别一上来就上BERT,那个推理延迟在网关层根本扛不住。

5. 避坑与排查:那些让我熬夜的翻车现场

5.1 训练集和测试集分布不一致

现象:离线AUC 0.98,上线后召回不到60%。原因:训练集正样本全来自靶场,测试集也是靶场,但线上流量里的注入payload风格完全不同——靶场用sqlmap默认payload,线上攻击者用手工构造的混淆payload。解决:训练集里必须混入真实流量的攻击样本,哪怕只有几百条。没有真实样本就用多种工具(sqlmap、手工、Burp插件)生成风格多样的payload,并且按攻击类型分层采样。

5.2 URL编码导致特征失效

现象:模型对%27这种编码后的引号完全不敏感。原因:TF-IDF在原始字符串上做,%27被当成三个独立字符,和'的特征空间不重叠。解决:在特征工程前做一次解码,把URL编码还原成原始字符,同时保留“是否经过编码”作为一个手工特征。注意别解码太狠,有些攻击就是靠双重编码绕过,解码一层即可,把编码层数记下来当特征。

5.3 正负样本时间泄漏

现象:模型在验证集上表现极好,但实际部署后效果差。原因:负样本和正样本来自同一时间段,模型可能学到了“这个时间段的请求都是正常的”这种伪特征。解决:按时间切分训练集和测试集,用早期数据训练,后期数据测试。如果数据量够,做时间序列交叉验证。这个坑在安全领域特别隐蔽,因为攻击往往集中在某些时段。

5.4 模型文件太大拖垮网关

现象:随机森林模型几百MB,网关加载后内存暴涨,推理延迟从1ms涨到50ms。原因:树模型对高维稀疏特征会生成大量节点。解决:限制树的数量和深度,或者用逻辑回归替代。如果必须用树模型,做特征选择,把TF-IDF的max_features从5000降到1000,AUC可能只掉1个点,但模型小一个数量级。另一个办法是模型蒸馏,用大模型教一个小模型。

5.5 误报把正常业务请求拦了

现象:上线第一天,运维反馈大量正常搜索请求被拦截。原因:正常搜索里用户会输入含引号、and、or的文本,模型把这些当成了注入。解决:加白名单机制,对已知的正常参数模式放行;同时把误报样本加入负样本重新训练。更根本的是,在特征里加入“参数是否在预期类型内”的判断,比如id参数应该是数字,出现字母就加分,但搜索参数本来就该是任意文本,不该用同一套阈值。

6. 在线推理与持续迭代:让模型跟着攻击者一起进化

模型训练完只是开始,真正难的是在线推理和持续迭代。推理层我一般用Flask或FastAPI包一个HTTP服务,网关把请求转发过来,模型返回分数,超过阈值就告警或拦截。下面是一个最小推理服务:

from flask import Flask, request, jsonify import joblib import numpy as np app = Flask(__name__) # 加载训练好的 vectorizer 和模型 char_vec = joblib.load("char_vec.pkl") word_vec = joblib.load("word_vec.pkl") clf = joblib.load("lr_model.pkl") @app.route("/detect", methods=["POST"]) def detect(): data = request.json raw = data.get("raw_request", "") if not raw: return jsonify({"error": "empty request"}), 400 # 特征转换必须和训练时完全一致 x_char = char_vec.transform([raw]) x_word = word_vec.transform([raw]) from scipy.sparse import hstack x = hstack([x_char, x_word]) prob = clf.predict_proba(x)[0, 1] # 阈值 0.5 是默认,实际部署建议根据误报率调整 return jsonify({"score": float(prob), "block": bool(prob > 0.5)}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)

这个服务的逻辑很直白:接收原始请求字符串,用训练时保存的vectorizer做transform,拼接后送模型,返回注入概率。参数上,阈值0.5是起点,真实环境要根据业务容忍度调——金融业务宁可误报不可漏报,阈值可以降到0.3;内部系统怕误报影响效率,可以升到0.7。注意vectorizer必须用joblib和模型一起保存,否则线上transform出来的特征空间和训练不一致,这是血泪教训。

持续迭代的关键是反馈闭环。每次告警让运维确认是真是假,确认结果自动回流到标注库,每周或每月重新训练一次。攻击手法在变,模型不更新,三个月后召回率就会明显下降。我一般会监控两个指标:一是线上请求的分数分布,如果正常请求的分数均值在漂移,说明数据分布变了;二是人工确认的误报率和漏报率,这两个指标比离线AUC更能反映真实效果。最后说个习惯:每次重新训练前,一定把新样本和旧样本混在一起跑一遍,确认旧样本上的表现没退化,否则模型可能学了新的忘了旧的。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询