预处理语句无法参数化表名/字段名等标识符,直接拼接会导致sql注入;唯一可靠方案是白名单校验或通过information_schema预查表存在性并严格限制权限。

动态SQL里拼接表名/字段名为什么危险
预处理语句(PDO::prepare、mysqli_prepare)只能参数化值,不能参数化标识符(如表名、字段名、ORDER BY 后的列名)。一旦用户输入被直接拼进 SELECT * FROM ${table_name} 或 ORDER BY ${sort_field},攻击者就能注入任意结构——比如把 table_name 设为 users WHERE 1=1 UNION SELECT password FROM mysql.user --,直接绕过业务逻辑读取系统表。
白名单校验是唯一可靠方案
别试图用 addslashes()、mysql_real_escape_string() 或正则过滤来“清理”标识符,MySQL 的标识符解析规则远比字符串复杂(支持反引号、Unicode、特殊分隔符),任何非白名单方式都可能被绕过。
- 提前定义合法值集合:例如排序字段只允许
['id', 'created_at', 'status'],查询表只允许['orders', 'products', 'customers'] - 运行时严格比对:
if (!in_array($user_input, $allowed_tables)) { die('Invalid table'); } - 避免“黑名单”式过滤(如禁止
information_schema)——攻击者可用INFORMATION_SCHEMA、infor/**/mation_schema等变体绕过
用元数据查表验证代替硬编码白名单
硬编码白名单在表结构频繁变更时难维护。更稳妥的做法是运行时查 information_schema.tables 或 sys.schema_table_statistics_with_buffer,确认用户请求的表真实存在且属于当前数据库:
SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?
注意两点:
- 这个查询本身必须用预处理,
?是表名参数,不是拼接 - 执行该查询的账号不能有跨库权限,否则攻击者可传入
mysql.user等敏感表名试探
存储过程里动态SQL更要严控DEFINER权限
如果业务必须用存储过程构造动态SQL(如报表生成),PREPARE + EXECUTE 绕过语法限制时,DEFINER 必须显式设为高权限账户(如 'admin'@'localhost'),且该账户不能拥有 GRANT OPTION 以外的全局权限。常见坑点:
-
SQL SECURITY INVOKER→ 执行时检查调用者权限,普通用户调用必然失败 -
DEFINER账户密码过期或权限被回收 → 存储过程静默失败,无日志提示 - 没做
FLUSH PRIVILEGES→ 新增用户后GRANT不生效,但错误不报
真正难防的不是语法错误,而是权限链路中某处松动后,动态SQL变成攻击者自由执行任意语句的跳板——它不依赖注入点,只依赖你给的那条 EXECUTE 路径。











