开发人员试图用正则表达式过滤SQL注入攻击,但写错了模式导致攻击者能轻易绕过。比如他们用preg_match('/union\s+select/i', $input)来拦截"union select",却忽略了//注释符也能当空格用——攻击者输入"union//select"就能直接穿透防御。更危险的是,有人把过滤写成if(preg_match('/drop|delete|update/', $sql)) return false;,结果漏了大小写变体"DrOp",连注释分割关键字"dr//op"都拦不住。
为什么正则表达式总在SQL注入防御上栽跟头?
根本矛盾在于正则表达式处理的是字符串模式匹配,而SQL解析器执行的是语法树分析。当开发人员用"/union\s+select/"去模拟SQL语法时,已经陷入三个认知陷阱:首先,SQL标准允许的空白符包括空格、制表符、换行以及注释块,但\s通常只匹配前三种;其次,数据库对关键字拆分具有容忍性,MySQL中"UNION"和"SELECT"之间甚至能插入"/*任意内容*/";最后,正则匹配默认按单行处理,但攻击者可以通过换行符构造"union%0aselect"绕过。更隐蔽的是字符集问题,某些UTF-8字符被解析后可能变成空白符,比如"union\xc2\x85select"在某些配置下能正常执行。
六类典型误写模式与穿透案例
1. 空白符匹配不全:使用"/\s+/"匹配连续空白时,Oracle数据库允许用"union(select"(括号作为分隔符),而正则表达式不会将括号识别为分隔符。攻击载荷:"union(select 1,2 from dual)--"
2. 注释符处理缺失:开发人员过滤了"/*"但忘记"*/"可单独出现。在SQL Server中"select/*注释*/1"合法,攻击者可构造"union/*绕过*/select"穿透"/union\s+select/"检测。进阶用法是用嵌套注释"/*!union select*/",MySQL会将其识别为特殊优化指令。
// 错误示范
if (preg_match('/\/\*.*\*\//', $sql)) {
die('检测到注释');
}
// 绕过方法:使用条件注释
$payload = "/*!50000union*//*!50000select*/ 1,2";3. 大小写转换逻辑漏洞:先转大写再匹配"UNION SELECT"的方案会被"UNunionION SELselectECT"这种嵌套写法破解。因为攻击者可以构造"ununionion select",应用程序移除"union"后正好剩下"union select"。防御代码如果采用str_ireplace()顺序替换,就可能被这种迭代移除漏洞利用。
4. 边界锚定错误:很多人用"/^union select/"检测开头位置,但攻击者可以在前面加换行符或无效字符:";%0aunion select 1--"。更精妙的是利用SQL解析特性:在"where id=1 union select"前插入"/*!00000"会使正则锚定失效。
5. 多重编码绕过:当应用层多次解码时,攻击者提交"%75%6e%69%6f%6e%20%73%65%6c%65%63%74"(URL编码的union select),某些WAF只做一次解码就会放过。如果配合双重编码"%2575%256e...",甚至能穿透三层检测逻辑。
// 典型漏洞链 $_GET['id'] = urldecode($input); // 第一次解码 $sql = htmlspecialchars_decode($_GET['id']); // 第二次解码 // 攻击载荷:%2527%2520union%2520select%25201 // 最终解析为:' union select 1
6. 回溯绕过:正则表达式"/union.{1,50}select/"本意是限制两个关键字距离,但攻击者用超长字符串"union/*"+ "a"*1000 +"*/select"可能触发正则引擎回溯超时,在某些配置下直接跳过检测。这种DoS式绕过在PHP的pcre.backtrack_limit设置过低时特别有效。
从语法解析视角重构防御逻辑
真正有效的方案是放弃字符串匹配,改用SQL词法分析思路。首先建立数据库关键字映射表,包含所有SQL保留字及其常见变体(如MySQL有370+保留字)。然后实现一个简易解析器,将输入按SQL词法单元拆解:识别字符串常量、数值常量、标识符、运算符、关键字,最后检查关键字序列是否包含危险组合。例如"union select"作为两个关键字,无论中间插入多少注释或空白,在词法分析阶段都会被识别为[KEYWORD_UNION, KEYWORD_SELECT]序列。
// 伪代码示例
function tokenizeSQL($input) {
$tokens = [];
$patterns = [
'comment' => '/\/\*.*?\*\/|\-\-[^\n]*/s',
'string' => '/\'([^\'\\\\]|\\\\.)*\'/',
'number' => '/\b\d+\b/',
'keyword' => '/\b(SELECT|UNION|INSERT|UPDATE|DELETE|DROP)\b/i',
];
// 移除注释(保留位置标记)
$cleaned = preg_replace($patterns['comment'], ' ', $input);
// 提取关键字并记录原始位置
preg_match_all($patterns['keyword'], $cleaned, $matches, PREG_OFFSET_CAPTURE);
return array_column($matches[0], 0);
}
// 检测逻辑
$tokens = tokenizeSQL($_GET['q']);
$dangerous_sequences = [
['UNION', 'SELECT'],
['INSERT', 'INTO'],
['DROP', 'TABLE']
];
foreach ($dangerous_sequences as $seq) {
if (hasSequence($tokens, $seq)) {
die('检测到SQL注入尝试');
}
}这种方法的关键优势在于:第一,统一将关键字转为大写或小写进行比较,避免大小写绕过;第二,注释在词法分析前已被移除,攻击者无法用注释分割关键字;第三,支持检测非连续的关键字组合,比如"union all select"也能被识别为危险序列。
三层防御体系的实际部署方案
第一层:输入规范化处理。对接收的所有参数进行统一编码标准化,包括:将全角字符转为半角、统一空白符为空格、移除控制字符(ASCII 0-31)、处理多重编码。特别要注意的是,必须固定解码顺序:先URL解码,再HTML实体解码,最后做字符集转换。这个过程中要记录转换日志,当发现参数经过多层解码才变得可读时,应当触发警报。
第二层:语义级检测引擎。基于开源SQL解析器(如PHP的PHPSQLParser)构建语法树,检查以下危险模式:
(1) 查询中包含多个不同来源的SELECT子查询;
(2) WHERE条件中出现永真表达式如"1=1";
(3) 出现非常规函数调用如load_file()、benchmark();
(4) 语句包含非常用关键字组合。对于预编译语句的参数绑定部分,要验证数据类型是否匹配占位符类型。
// 使用解析器检测示例
require_once 'PHPSQLParser.php';
$parser = new PHPSQLParser();
try {
$parsed = $parser->parse($_POST['query']);
// 检查是否包含多个SELECT语句
if (count($parsed['SELECT']) > 1) {
log_attack('多语句查询尝试');
}
// 检查WHERE条件是否包含恒真式
foreach ($parsed['WHERE'] as $condition) {
if (is_always_true($condition)) {
block_request();
}
}
} catch (Exception $e) {
// 解析失败可能是畸形SQL
require_human_verification();
}第三层:运行时行为监控。在数据库驱动层拦截最终执行的SQL,监控以下异常:
(1) 单次请求中相同模板的SQL语句执行超过阈值(如5次);
(2) 查询结果集大小突然增长百倍以上;
(3) 出现敏感表访问(user、password等表);
(4) 执行时间异常长。这层防御能捕获前两层漏过的攻击,特别是那些利用应用程序业务逻辑缺陷的二次注入。
正则表达式的正确使用边界
正则表达式并非完全无用,但应该限定在特定场景:第一,用于检测明显的恶意模式,如"exec xp_cmdshell"这种与业务无关的操作;第二,在白名单验证中,用正则验证参数格式是否符合预期,例如"/^\d+$/"验证数字ID;第三,在日志分析阶段批量识别攻击模式。关键原则是:正则表达式应该用于"验证合法"而非"检测非法",因为攻击模式是无穷的,但合法模式是有限的。
一个值得推荐的实践是采用"正则表达式+异常词库"的双重检测:先用正则快速过滤掉99%的常规攻击,再用基于机器学习的异常检测分析剩余请求。例如,可以训练一个模型识别SQL关键字的异常分布——正常查询中"SELECT"通常出现在开头附近,而注入攻击中"SELECT"可能出现在WHERE子句或ORDER BY后面。
最后必须强调的是,任何基于正则表达式的过滤都应该视为临时补充措施,绝不能替代参数化查询。主流数据库驱动都支持预编译语句,比如PDO的prepare()、MySQLi的stmt_init(),这些才是解决SQL注入的根本方案。当历史遗留系统无法改造时,可以考虑在数据库前部署代理层(如ProxySQL),在SQL协议层面进行语法验证,这比在应用层做字符串匹配可靠得多。
