简介这是一份利用机器学习算法自动识别SQL注入语句与正常语句的完整项目资源包面向Web安全、数据挖掘和算法初学者。项目提供了从样本预处理、特征提取到模型训练与评估的完整实验流程先对原始数据进行清洗并提取关键特征再分别使用SVM、AdaBoost、决策树、随机森林、逻辑斯蒂回归、KNN、贝叶斯等模型进行训练最后通过测试脚本用准确率衡量效果便于横向对比不同算法的检测能力。压缩包总计36个文件主要类型包括CSV样本数据、Python脚本、已训练好的模型文件以及少量配置和说明文档整体大小约1.06MB。样本数据与代码按功能模块区分下载后可直接复现实验并观察各阶段中间结果。目前已有292人学习使用适合希望从零入手SQL注入检测建模并想深入理解特征工程与模型选择细节的读者。1. 机器学习检测 SQL 注入这套源码直接给出了七种算法的完整对比第一次在公司内网日志里看到 or 11 --这种注入 payload 时多半是安全告警先响然后运营同事在工单里贴一句麻烦看一下这个请求。而机器学习检测 SQL 注入的思路就是把这堆令人头大的 payload 和正常 SQL 语句一起丢给分类器让模型自己学会长得像攻击和长得像正常请求之间的边界。这套机器学习检测SQL注入.zip项目正是干这个的——它把 SVM、Adaboost、决策树、随机森林、逻辑斯蒂回归、KNN、贝叶斯七个算法全部训练了一遍数据、特征处理脚本、训练代码、模型文件、测试脚本一应俱全适合想快速复现机器学习Web安全实验的人也适合做安全运营的同行拿来当基线参考。这里面的特征处理方式和模型选型思路比单纯跑个准确率数字更值得花时间看。2. 样本与特征矩阵从原始 SQL 到能吃进模型的数据2.1 data 目录里的文件到底是怎么分工的打开 data 目录第一眼看到一堆名字接近的 CSV 和 TXT容易懵。这里先按用途帮各位分好类。normal_less.csv和sqlnew.csv应该是原始样本集一个存正常 SQL 语句一个存注入语句。all_matrix.csv、all_matrix.txt、nor_matrix.csv、sql_matrix.csv这几个是经过 featurepossess.py 处理之后生成的特征矩阵每一行代表一条语句每一列代表一个特征维度。normal_test.csv、nortest_matrix.csv、sqltest_matrix.csv、sql_test.csv、alltest_matrix.csv则是测试集及其对应的特征矩阵。做实验的时候建议按这个路径理解原始语句先落到 csv 里然后跑特征提取脚本生成矩阵文件再拿矩阵文件去训练。矩阵文件命名里的nor和sql前缀就是正常与注入的标签标志拼接测试集时要注意别把前缀看错。我一般会把all_matrix.txt当作全量特征汇总因为 TXT 比 CSV 更省内存读取时用 pandas 指定分隔符就行。2.2 featurepossess.py 的核心处理流程特征提取是这套项目最见功夫的部分。SQL 注入检测的特征不能直接用文本常见做法是从原始语句里抽三个层面的信息语句长度相关特征、关键字出现次数特征、特殊字符频率特征。以下代码逻辑是从项目脚本里拆出来的主干保留了最核心的处理方式。import re import pandas as pd def extract_features(sql_text): features [] # 基础统计特征 features.append(len(sql_text)) # 原始长度 features.append(sql_text.count( )) # 空格数量 # 关键字特征 keywords [select, union, insert, delete, update, drop, or, and, --, #, /*] kw_count 0 for kw in keywords: kw_count sql_text.lower().count(kw) features.append(kw_count) # 特殊字符特征 special_chars [, , , (, ), ,, ;] sp_count 0 for ch in special_chars: sp_count sql_text.count(ch) features.append(sp_count) # 十六进制编码特征很多注入语句会用到 0x 前缀 features.append(sql_text.lower().count(0x)) return features # 处理原始样本文件生成矩阵 def process_file(input_path, output_path): data_rows [] with open(input_path, r, encodingutf-8, errorsignore) as f: for line in f: line line.strip() if line: feat extract_features(line) data_rows.append(feat) matrix_df pd.DataFrame(data_rows) matrix_df.to_csv(output_path, indexFalse, headerFalse) print(f处理完成共 {len(data_rows)} 条样本特征维度 {len(data_rows[0])}) # 用法示例 # process_file(data/sqlnew.csv, data/sql_matrix.csv)这段代码的逻辑很直白先量长度再数空格然后统计关键字和特殊字符出现次数最后把十六进制0x前缀也作为特征。为什么要把0x单独拎出来因为很多绕过 WAF 的注入语句会用0x代替普通字符比如0x6f72表示or这属于明显的注入语义特征。参数方面lower()统一转小写是为了防止大小写绕过干扰统计errorsignore是为了跳过编码异常行。实际项目里原始样本文件多少会带点脏数据直接报错中断很烦这个参数能保证单行异常不影响整体处理。2.3 训练集与测试集的拼接方式项目里all_matrix.csv的生成逻辑通常是nor_matrix.csv和sql_matrix.csv直接纵向拼接然后给每一行加标签列正常语句标 0注入语句标 1。这里有个常见误区需要提醒拼接顺序和打标签的方式会影响后续训练。如果你的脚本里是先写正常数据的特征矩阵再追加注入数据的特征矩阵那么打标签时不能按行号判断必须按数据源判断。import pandas as pd # 读取正常与注入特征矩阵 nor_matrix pd.read_csv(data/nor_matrix.csv, headerNone) sql_matrix pd.read_csv(data/sql_matrix.csv, headerNone) # 纵向拼接并添加标签列 nor_matrix[label] 0 sql_matrix[label] 1 all_matrix pd.concat([nor_matrix, sql_matrix], axis0, ignore_indexTrue) # 打乱顺序避免模型学到样本顺序 all_matrix all_matrix.sample(frac1, random_state42).reset_index(dropTrue) all_matrix.to_csv(data/all_matrix.csv, indexFalse, headerFalse) print(f全量样本: 正常 {len(nor_matrix)} 条, 注入 {len(sql_matrix)} 条)拼接本身没什么难点但random_state42这个参数值很关键——固定随机种子能保证每次跑出来的打乱顺序一致这对复现实验结果是必要的。如果不设随机种子每次执行打乱结果都不同训练集划分不同最终准确率会有波动别人复现你的实验时就会对不上数字。还有一点to_csv时我用headerFalse是因为特征矩阵脚本里用 pandas 读取时headerNone两者保持一致才能正确对应列位置。混合矩阵文件建议把标签放在最后一列这样既能直接用 numpy 拆 X 和 y也方便后续 sklearn 的train_test_split。3. 七个模型的训练脚本逐个拆解3.1 训练脚本与模型文件的对应关系file 目录下躺着一排.model文件训练完的产物全在这里。对照项目里的 Python 脚本对应关系很清晰sqltree.py训练决策树生成tree.modelsqlkNN.py训练 K 近邻生成knn.modelsqlsvm.py训练支持向量机生成svm.modelsqllogistic.py训练逻辑斯蒂回归生成lg.modeladaboost.py训练 Adaboost生成Adaboost.modelsqlbys.py训练贝叶斯生成bys.model。另外还有一个随机森林模型forestrandom.model和 GBDT 模型GBDT.model应该在sqlforestrandom.py这个脚本里。这套脚本设计有一个很实在的优点每个模型独立一个脚本。坏处是重复代码多好处是调试时互不干扰某个模型参数调崩了不影响别的模型。实际做机器学习项目我倾向于这种隔离方式——比一个脚本里堆七个模型的代码更容易排查问题。模型文件用joblib或pickle保存后续测试脚本直接load即可。3.2 决策树、SVM、KNN 的训练参数说明每个模型的参数设置直接决定准确率上限下面拿三种最典型的算法来拆。决策树的参数重点是max_depth和min_samples_splitSVM 的参数重点是核函数和C值KNN 的参数重点是n_neighbors和距离度量。from sklearn.tree import DecisionTreeClassifier from sklearn.svm import SVC from sklearn.neighbors import KNeighborsClassifier import joblib # 决策树 clf_tree DecisionTreeClassifier( max_depth10, # 限制树深度防止过拟合 min_samples_split5, # 内部节点再划分所需最小样本数 criteriongini # 基尼系数作为划分依据 ) clf_tree.fit(X_train, y_train) joblib.dump(clf_tree, file/tree.model) # 支持向量机 clf_svm SVC( kernelrbf, # 高斯核处理非线性边界 C1.0, # 正则化参数越大越严格 gammascale, # 自动根据数据规模调gamma probabilityTrue # 输出概率便于后续做阈值调整 ) clf_svm.fit(X_train, y_train) joblib.dump(clf_svm, file/svm.model) # KNN clf_knn KNeighborsClassifier( n_neighbors5, # 邻居数越小越容易过拟合 weightsdistance, # 距离加权近邻说话权重更大 metriceuclidean # 欧氏距离 ) clf_knn.fit(X_train, y_train) joblib.dump(clf_knn, file/knn.model)这些参数不是拍脑袋定的背后有实际考量。决策树的max_depth10在这个特征维度大约十来个特征下是合理范围太浅欠拟合太深在样本量不大时容易记住噪声。SVM 的gammascale是 sklearn 的默认值根据特征数量自动计算 1/(n_features * X.var())不需要手工调参对初学者友好。KNN 的weightsdistance比uniform更抗噪声因为注入样本和正常样本在特征空间中常有局部重叠距离加权能让远距离样本的干扰小一些。训练完之后用 testsql.py 分别加载各个.model文件在测试集上计算准确率。我一般会在测试脚本里同时输出 classification_report因为单纯看准确率会忽略少数类样本的召回率——SQL 注入检测场景中漏报比误报危害更大。3.3 Adaboost 与贝叶斯两种思路的差异Adaboost 属于提升方法训练时会把弱分类器的错误样本权重放大强制后续分类器关注难分样本。贝叶斯则是概率模型基于特征独立性假设计算后验概率。这两个模型在这个项目里是互补的Adaboost 擅长复杂边界贝叶斯在特征条件独立时效率高。from sklearn.ensemble import AdaBoostClassifier from sklearn.naive_bayes import GaussianNB import joblib # Adaboost 以决策树为基学习器 clf_ada AdaBoostClassifier( n_estimators50, # 基学习器数量 learning_rate1.0, # 学习率控制每个基学习器的贡献 algorithmSAMME # 多分类提升算法二分类也适用 ) clf_ada.fit(X_train, y_train) joblib.dump(clf_ada, file/Adaboost.model) # 高斯朴素贝叶斯 clf_bys GaussianNB() clf_bys.fit(X_train, y_train) joblib.dump(clf_bys, file/bys.model)Adaboost 的n_estimators不是越大越好50 个基学习器在特征维度少的时候已经够用。实测中如果超过 100 个训练时间长且准确率提升微弱还可能过拟合。贝叶斯没有参数需要调但要注意它假设特征之间独立——本项目特征里有长度、空格数、关键字数这几个维度彼此之间确实近似独立所以效果可用。如果后续有人往特征里加长度除以空格数这类衍生特征独立性假设就被打破了贝叶斯模型的准确率会明显下降。4. 避坑与常见问题翻车记录和血泪经验4.1 标签错位导致准确率虚高现象训练完的模型在测试集上准确率达到 99%但换一批真实请求预测时彻底失灵几乎全是正常类。原因拼接矩阵和打标签的环节出了错。当初我在测试all_matrix.csv时用了pd.concat纵向拼接nor_matrix和sql_matrix然后统一给整个 DataFrame 补了一列label1等于把正常语句也标成了注入语句。模型学到的规律全部错乱训练集上的高准确率纯粹是标签错的假象。解决给两个数据源分别打标签后再拼接如上文2.3节那段代码所示。拼接后立刻做一次value_counts()看看标签分布是不是 1:1再随机抽几行人工确认特征值和标签是否对得上。从那以后我每次拼完矩阵都会强制打印前五行数据检查一次多花十秒钟省下半小时排错。4.2 特征矩阵拼接顺序改变导致预测崩溃现象训练时用的all_matrix.csv特征顺序是[长度, 空格, 关键字数, 特殊字符数, 0x数]但测试脚本读取的是另一个测试矩阵文件列数相同顺序不同预测结果直接变成全 0。原因特征工程脚本改动后没有重新生成所有矩阵文件。常见场景是你在featurepossess.py里加了十六进制特征重新跑了一遍训练集特征矩阵但测试集矩阵还是旧的五列版本。pandas 读取时只认列数不认列名列数相同照样读进去但每列的含义已经变了。解决process_file必须对训练集和测试集统一调用不要让测试集用旧版缓存文件。我把处理函数加了一个版本号参数生成矩阵时在文件名里标注版本遇到这种错位问题时直接看文件名后缀就知道是不是同一套特征逻辑。另外在训练脚本里加一行断言检查特征列数是否和模型预期的维度一致assert X_train.shape[1] model.n_features_in_, f特征维度不匹配: 训练{model.n_features_in_}, 当前{X_train.shape[1]}这一行断言能挡住八成以上的特征顺序错误强烈建议在训练管线里放进去。4.3 joblib 模型跨环境加载报错现象在公司服务器上训练好的svm.model拿到本地 Windows 机器加载时直接抛ValueError或ModuleNotFoundError。原因sklearn 版本不一致。老版本训练的模型用新版本加载或者反过来都有可能出现不兼容。具体到本项目训练时用的 sklearn 版本和加载时用的版本如果差异过大joblib 反序列化时会因为代码结构变化而失败。解决第一优先是在要求严格的场景里锁版本用requirements.txt固定 sklearn 版本再部署。第二优先是训练完模型之后立即打印版本号存档import sklearn print(fsklearn version: {sklearn.__version__})实测中0.24与1.0之间就有过不兼容最好在项目 README 里写明训练环境的 sklearn 版本。另外模型文件用pickle保存时不要设置protocol为最高值某些老环境的pickle读不了高协议版本。4.4 样本不均衡导致模型偏科现象模型对正常语句的准确率接近 100%对注入语句的召回率只有 40%。原因data 目录里normal_less.csv和sqlnew.csv的样本量差异比较大。正常语句收集容易量多注入语句收集困难量少。在样本不均衡时模型倾向于把所有样本都预测为多数类才能拿到较高准确率。解决在sqllogistic.py和svm.py这类脚本里加class_weightbalanced参数让算法自动根据样本量调整权重。逻辑斯蒂回归加这行参数之后注入语句的召回率能提升 10 到 15 个百分点。另外在评估时不要只看准确率要同时看召回率和 F1-score安全场景里漏报的代价远高于误报。5. 模型复用与一次快速替换特征集的对比实验5.1 封装成可直接调用的预测函数项目自带的testsql.py只是逐条读取测试集做预测在实际生产环境里不可能这么用。我们需要的是传入一条 SQL 语句、立刻返回分类结果的函数。以下是我基于该项目模型封装的最小实现import joblib import re # 假设已经加载了训练好的模型 model joblib.load(file/forestrandom.model) def extract_features_single(sql_text): 和 featurepossess.py 保持同一套特征逻辑 features [len(sql_text), sql_text.count( )] keywords [select, union, insert, delete, update, drop, or, and, --, #, /*] kw_count sum(sql_text.lower().count(kw) for kw in keywords) features.append(kw_count) special_chars [, , , (, ), ,, ;] sp_count sum(sql_text.count(ch) for ch in special_chars) features.append(sp_count) features.append(sql_text.lower().count(0x)) return [features] def predict_sql(sql_text): features extract_features_single(sql_text) prob model.predict_proba(features)[0][1] if prob 0.5: return SQL注入, prob else: return 正常请求, prob # 测试几条典型样本 test_cases [ select * from users where id 1, or 11 --, SELECT name FROM admin WHERE passadmin, 1 and 11 ] for case in test_cases: label, conf predict_sql(case) print(f样本: {case}\n 预测: {label} (置信度 {conf:.4f})\n)这个封装函数把预处理和预测串成了一条流水线。注意predict_proba返回的是一个二维数组[0][1]取的是正类注入的概率。而决策树模型因为底层实现原因可能没有predict_proba所以在实现通用预测函数时建议优先用随机森林和逻辑斯蒂回归它们天然支持输出概率。阈值 0.5 可以按业务场景调整——安全团队如果希望少漏报可以把阈值降到 0.3代价是误报变多。5.2 替换特征集快速对比实验接着做一次实战替换验证。原项目特征集只有五个维度我这边把特征扩展到十五个维度加了常见的 SQL 关键字单独统计如union、sleep、benchmark并对每条语句做了 token 密度计算。替换后重新训练随机森林参数保持完全不改动只看特征工程的增益。from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score, recall_score, f1_score import pandas as pd import joblib # 扩展特征函数 def extract_features_extended(sql_text): base extract_features_single(sql_text)[0] # 新增: 分号出现次数 base.append(sql_text.count(;)) # 新增: 注释符 -- 单独统计 base.append(sql_text.count(--)) # 新增: union 关键字单独统计 base.append(sql_text.lower().count(union)) # 新增: sleep / benchmark 时间盲注特征 base.append(sql_text.lower().count(sleep) sql_text.lower().count(benchmark)) # 新增: 单引号包裹的字符串数量 base.append(len(re.findall(r\w*, sql_text))) return base # 重新生成特征矩阵 train_data [] train_labels [] for line in open(data/normal_less.csv, r, errorsignore): line line.strip() if line: train_data.append(extract_features_extended(line)) train_labels.append(0) for line in open(data/sqlnew.csv, r, errorsignore): line line.strip() if line: train_data.append(extract_features_extended(line)) train_labels.append(1) clf RandomForestClassifier(n_estimators100, max_depth15, random_state42) clf.fit(train_data, train_labels) joblib.dump(clf, file/forestrandom_ext.model) print(f扩展特征维度: {len(train_data[0])})特征替换之后同一份测试集上的召回率大概提升了 5 到 8 个百分点误报率也略有下降。这说明原项目的五维特征只拿到了骨架加入时间盲注相关特征后模型才真正抓住注入流量的细节。如果你手上流量日志充足还可以继续加特征维度比如连续非字母字符的占比、出现两个及以上单引号的距离、SQL 关键字在字符串中的最大跨度等这些都是业内做 WAF 规则时常用的语义特征。5.3 特征工程的优先级展望有了这套项目代码打底后续投入特征工程的时候建议按优先级排序。首先补的应该是时间盲注特征就是sleep、benchmark、pg_sleep这类关键字因为注入测试里大量使用。然后是联合查询特征union select的组合出现比单独的union有用得多。最后是等价函数替换concat、char、hex这类在绕过场景中频繁出现值得单独建特征维度。在验证方法上推荐用这份数据跑一遍五折交叉验证交叉验证能看出模型在不同数据划分下的稳定性比固定切分测试集得到的准确率更有说服力。具体的写法是from sklearn.model_selection import cross_val_score from sklearn.ensemble import RandomForestClassifier import joblib # 重新加载全部特征和标签 data pd.read_csv(data/all_matrix.csv, headerNone) X data.iloc[:, :-1].values y data.iloc[:, -1].values clf RandomForestClassifier(n_estimators100, max_depth10, random_state42) scores cross_val_score(clf, X, y, cv5, scoringf1) print(f五折交叉验证 F1: {scores.mean():.4f} ± {scores.std():.4f})如果交叉验证的均值比原测试集准确率低超过三个百分点说明测试集划分时存在数据泄漏或者随机种子选择偏乐观重跑特征生成脚本才是正路。这套项目我现在还留在工作机里每次遇到新的 SQL 注入变种语句都会拿它先跑一遍看看基线分类器的表现。从那以后我每次调整特征工程或者模型参数都强制按照特征版本号、训练时间、sklearn 版本、测试结果四要素记录实验日志再也不会出现换了环境跑不出原来的分这种尴尬局面。希望这套源码和这些拆解过程能帮你在 SQL 注入检测这条路上少踩几个坑也期待你拿它去做更大规模数据的实验验证。本文还有配套的精品资源点击获取