sql注入本质是用户输入被当作代码执行,单靠过滤转义无法根治,必须通过参数化查询切断“输入→代码”路径,并对无法参数化的动态结构(如表名、排序字段)实施白名单校验。

SQL注入不是“输入没过滤”这么简单,而是应用层把用户输入当代码执行了——只要拼接字符串构造SQL,就存在被当代码解析的风险。彻底修复不靠加一层过滤,而在于切断“输入→代码”的路径。
为什么单靠过滤和转义永远修不干净
过滤(比如删掉单引号)和转义(比如把'变成\')都发生在应用层,但数据库解析时仍可能绕过:MySQL的宽字节、Oracle的CHR()、PostgreSQL的Unicode编码都能绕过常见正则;更麻烦的是,像ORDER BY、GROUP BY、表名、字段名这些位置根本不能转义——它们本身就是SQL语法的一部分。
- 用
preg_replace("/[';#--]/", "", $input)挡不住%df%27(GBK宽字节注入) -
mysql_real_escape_string()在PHP 7.4+已废弃,且对非字符串上下文无效 - 对
$_GET['sort']做htmlspecialchars()毫无意义——它要进SQL,不是进HTML
参数化查询必须覆盖所有动态位置
参数化不是“用一下prepare()就行”,而是每个用户可控的值,无论出现在WHERE、IN、LIMIT还是OFFSET,都必须走绑定参数。MySQLi和PDO默认支持,但要注意陷阱:
- PDO默认开启模拟预处理(
PDO::ATTR_EMULATE_PREPARES = true),此时prepare()只是字符串替换,不防注入;必须显式关闭:new PDO($dsn, $u, $p, [PDO::ATTR_EMULATE_PREPARES => false]) - MySQLi中
mysqli_prepare()只支持单条语句,UNION SELECT或堆叠查询(;分隔)无法参数化,这类场景必须用白名单校验 -
LIMIT ?在MySQL中不支持参数化,必须用intval()或范围检查后硬编码
动态SQL结构必须用白名单硬控制
字段名、表名、排序方向(ASC/DESC)、分组维度这些没法参数化的部分,唯一安全做法是限定死可选值:
- 排序字段:
$allowed_sorts = ['id', 'created_at', 'status']; if (!in_array($_GET['sort'], $allowed_sorts)) die('invalid sort'); - 导出格式:
$format = in_array($_GET['fmt'], ['csv', 'json', 'xlsx']) ? $_GET['fmt'] : 'csv'; - 多表关联:
$join_map = ['user' => 'users', 'profile' => 'user_profiles']; $table = $join_map[$_GET['join']] ?? 'users';
别信preg_match('/^[a-z_]+$/', $input)——users; DROP TABLE users--也能过这个正则。
生产环境必须关闭错误回显并捕获所有异常
报错注入之所以危险,是因为MySQL Error 1064或ORA-00904直接暴露表结构。修复不是“不让报错”,而是不让错误信息流到前端:
- PHP里确认
display_errors = Off且error_reporting不含E_WARNING或E_NOTICE - 所有DB操作必须包在
try/catch里,catch块只写日志,绝不echo $e->getMessage() - 用
mysqli_error()或PDO::errorInfo()前先检查是否在调试模式——线上环境一律跳过
最容易漏的是分页接口的$_GET['page']、搜索接口的$_GET['q']、图表API的$_POST['metric']——这些边缘参数一旦拼进SQL又没兜底,就是报错注入的入口。











