能,但仅限参数化部分有效;prepare 本身不防注入,execute ... using 中的 ? 占位符才起作用,动态结构如表名、列名等必须白名单校验。

PREPARE / EXECUTE 能防 SQL 注入吗?
能,但只在参数化部分有效——PREPARE 本身不防注入,真正起作用的是 EXECUTE ... USING 中的占位符机制。如果你把用户输入拼进 PREPARE 的 SQL 字符串里(比如 CONCAT('SELECT * FROM users WHERE id = ', user_input)),那跟字符串拼接没区别,照样中招。
常见错误现象:ERROR 1064 (42000): You have an error in your SQL syntax 或更隐蔽的逻辑绕过,比如传入 1 OR 1=1 导致全表泄露。
- 正确用法:SQL 模板固定,仅变量走
USING参数,如PREPARE stmt FROM 'SELECT * FROM orders WHERE status = ? AND created_at > ?' - 错误用法:把字段名、表名、排序方向等动态内容塞进
PREPARE字符串——这些无法用?占位,必须另做白名单校验 - MySQL 8.0+ 支持
EXECUTE ... USING @var1, @var2,但@var仍需确保是干净值,不能是拼出来的 SQL 片段
哪些东西根本不能用 ? 占位,必须手动过滤?
列名、表名、ORDER BY 字段、GROUP BY 表达式、LIMIT 偏移量(非数值本身)、UNION 后的列数对齐——这些语法结构在解析阶段就需确定,MySQL 不允许用 ? 替代。
典型使用场景:后台支持按不同字段排序的报表接口,或租户隔离下动态切换表前缀。
- 安全做法:建立严格白名单,比如排序字段只允许
['created_at', 'amount', 'status'],用IN或CASE显式比对 - 避免用正则“过滤掉危险字符”,因为
`、"、/* */等都可能被绕过;也不要用mysql_real_escape_string(已废弃)或简单str_replace - 若真要拼表名,优先用配置映射而非原始输入:用户传
report_type=monthly→ 后端查配置得table_name = 'reports_monthly_v2'
拼字符串 + 手动转义为什么大概率翻车?
因为 SQL 字符串拼接的上下文太多:单引号、双引号、反斜杠、十六进制编码、宽字节截断、注释符号嵌套……任何一处漏判,就等于给攻击者留了后门。
性能影响不大,但维护成本极高——每次加个新字段就得重新 review 转义逻辑,且不同 MySQL 版本对特殊字符的处理有细微差异。
- 例如:用户输入
O'Reilly,你用addslashes()得到O\'Reilly,看似安全,但在 GBK 编码下可能被构造为%A1%5C绕过 -
QUOTE()函数可安全包裹字符串值,但它不能用于标识符(列/表名),也不能防止二次注入(比如存进数据库后再拼进另一条 SQL) - 最省心的方式:彻底分离“结构”和“数据”——结构由代码或配置决定,数据全走
USING;实在绕不开动态结构,就用预定义枚举 + 显式 switch
动态 SQL 在存储过程中怎么写才不容易出事?
存储过程里用 CONCAT() 拼 SQL 是高危操作,尤其当参数来自 IN 参数或用户表字段时。MySQL 不会校验拼出来的语句是否合法,直到 EXECUTE 才报错,而此时权限已生效。
兼容性注意:MySQL 5.7 默认关闭 sql_mode=STRICT_TRANS_TABLES,某些非法拼接可能静默截断,导致行为偏差。
- 原则:所有拼接内容先过白名单函数,比如自定义
VALIDATE_IDENTIFIER(in_str)返回 TRUE/FALSE,内部用REGEXP '^[a-zA-Z_][a-zA-Z0-9_]*$'校验 - 避免直接
SET @sql = CONCAT('SELECT * FROM ', in_table_name);改用IF in_table_name NOT IN ('orders', 'users', 'products') THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid table'; END IF; - 如果必须支持任意字段,考虑用视图或生成临时表,而不是实时拼 SQL —— 把动态压力从执行时转移到部署时
真正难的不是写对那几行 PREPARE,而是想清楚哪部分该死守白名单、哪部分能放心交给 USING。边界模糊的地方,宁可多查一次配置表,也不要少做一次校验。










