旧版PHP系统无法添加SQL注入防护中间件,必须将参数化查询封装为safe_query()函数并全局替换所有mysqli_query()调用,同时对动态表名、列名、LIKE、IN、ORDER BY等场景做白名单校验与预处理控制。

旧版PHP系统没法直接上ORM或框架,所谓“SQL注入防护中间件”本质上是伪概念——没有标准接口、不支持自动拦截、无法在PDO/MySQLi底层注入钩子。你真正能做的,是把参数化查询能力封装成可复用的函数层,并强制覆盖所有现有SQL调用点。
为什么不能加“中间件”而必须改代码
PHP 5.6–7.4 的老旧系统普遍直接调用 mysql_query() 或裸写 mysqli_query(),这些函数不经过任何抽象层,也没有事件钩子或执行前拦截机制。所谓“中间件”,在无自动请求路由、无统一DB入口、无autoload机制的老系统里根本无处挂载。
常见错误现象包括:
- 试图用
set_error_handler()捕获SQL错误并反向修复——它只捕获PHP错误,不干预SQL拼接过程 - 写个全局函数如
safe_sql()包裹原始SQL字符串——若没重写所有mysqli_query()调用,90%的漏洞点依然裸奔 - 依赖WAF规则过滤
' OR '1'='1——绕过方式极多(编码、注释、宽字节、大小写混用),且无法防御INSERT或UPDATE场景下的数据篡改
用 PDO 封装层替代原生 mysqli_query
不是加中间件,而是替换调用链。目标:让所有 mysqli_query($conn, $sql) 都变成走预处理逻辑,且不改动业务代码结构。
立即学习“PHP免费学习笔记(深入)”;
实操建议:
- 新建
db_safe.php,定义safe_query($conn, $sql, $params = [])函数,内部用$conn->prepare($sql)+execute($params)实现 - 对原系统中所有
mysqli_query()调用,用 Cursor 全局搜索+替换:mysqli_query\(([^)]+),\s*([^)]+)\)→safe_query($1, $2) - 注意兼容性:若原SQL含动态表名或列名(如
"SELECT * FROM {$_GET['table']}"),$sql参数不能进占位符,必须先用白名单校验:in_array($_GET['table'], ['users', 'orders']),再拼接 - 禁用模拟预处理:
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false),否则 MySQL 会退化为客户端转义,宽字节注入仍可能生效
处理 LIKE、IN、ORDER BY 这类动态语法
占位符不能用于表名、列名、排序方向、IN 子句长度——这些必须在PHP层硬控制,否则生成的SQL模板本身就不合法。
典型场景与应对:
-
LIKE查询:用户输入$_GET['kw'],不能写成"WHERE name LIKE '%?%'"(语法错误),要先拼好通配符:$search = '%' . $_GET['kw'] . '%';,再绑定:$stmt->execute([$search]) -
IN (?, ?, ?):若ID数组长度不确定,需动态生成占位符串:$placeholders = str_repeat('?,', count($ids) - 1) . '?';,再拼SQL:"WHERE id IN ($placeholders)",最后绑定整个数组 -
ORDER BY:字段名和方向必须白名单限定,例如:$field = in_array($_GET['sort'], ['name', 'created_at']) ? $_GET['sort'] : 'id';,$dir = in_array($_GET['dir'], ['ASC', 'DESC']) ? $_GET['dir'] : 'DESC';,然后硬编码进SQL:"ORDER BY $field $dir"
绕过风险最高却最容易被忽略的点
最危险的不是登录框,而是那些看起来“只读”、不涉及用户提交的接口——比如后台导出Excel时拼接 WHERE date >= '{$_GET['start']}',或者日志页按 $_SERVER['HTTP_USER_AGENT'] 记录并查询。这类地方往往没做任何过滤,又恰好在管理员权限下运行,一旦突破就是RCE级后果。
排查时盯死三类变量:
- 所有
$_GET、$_POST、$_REQUEST、$_COOKIE直接参与字符串拼接的位置 - 服务器环境变量:
$_SERVER['HTTP_REFERER']、$_SERVER['HTTP_USER_AGENT']、$_SERVER['QUERY_STRING'] - 文件路径、配置项读取结果:如
parse_ini_file($_GET['cfg'])后再拼SQL,等于把任意文件内容拖进数据库上下文
别信“这个字段前端限制了长度”——后端永远要当输入是恶意的。一个 maxlength="20" 的输入框,curl 一发就能绕过。



















