单纯转义不能根治sql注入,因攻击者可用十六进制、注释符绕过,且宽字节、多层编码、上下文限制及数据库差异均使其失效;根本解法是参数化查询分离结构与数据,并对表名、排序字段等结构部分严格白名单校验。

为什么单纯转义不能根治SQL注入
转义只是把单引号变成 \'、把分号变成 \; 这类表面处理,但攻击者早就能绕过——比如用十六进制编码 0x27 代替单引号,或用注释符 -- 或 # 截断后续校验逻辑。更麻烦的是,不同数据库对转义规则支持不一(MySQL 的 mysql_real_escape_string 在宽字节场景下可能失效),而前端、中间件、数据库多层编码不一致时,一次转义还可能被二次解码“还原”。
- 转义函数只对特定上下文有效(如仅用于字符串字面量,不适用于表名、列名、ORDER BY 子句)
- UTF-8 和 GBK 混用时,
%A1%AA这类双字节 payload 可能触发宽字节注入,让转义失效 - JSON 接口里用户输入若拼进 SQL,转义根本没机会生效——因为 JSON 解析已先完成,SQL 才刚起步
真正该用的不是 addslashes,而是参数化查询
参数化查询把 SQL 结构和数据彻底分离:数据库先编译好语句模板(比如 SELECT * FROM users WHERE id = ?),再把用户输入作为独立参数传入,根本不参与 SQL 解析阶段。这意味着哪怕用户输的是 1 OR 1=1; DROP TABLE users--,数据库也只会把它当一个字符串值去比对,绝不会执行。
- PHP 用
PDO::prepare()+bindValue(),严禁把变量直接拼进query()字符串 - Java 用
PreparedStatement,调用setString()/setInt(),别用Statement拼接 SQL - Python 用
cursor.execute("SELECT ... WHERE name = %s", [name]),别用f"WHERE name = '{name}'" - Node.js 用
pool.query("SELECT ... WHERE id = $1", [id])(PostgreSQL)或?占位符(MySQL2)
哪些地方参数化也救不了你——必须白名单+拒绝动态拼接
参数化只管“值”,不管“结构”。一旦你要让用户决定查哪张表、按哪个字段排序、用什么条件组合,就等于把 SQL 的骨架交出去了。这时候任何转义或参数化都无效。
- 表名/列名不能参数化:必须用白名单校验,比如
if (sortField !== 'created_at' && sortField !== 'score') throw Error('invalid field') - 动态
WHERE条件组合,别拼 SQL 字符串,改用 Query Builder(如 Knex、MyBatis 的<if></if>标签)或预定义策略枚举 - 存储过程虽能限制执行范围,但若内部仍用
EXEC(@sql)拼接,照样中招 - 日志、监控、审计系统里若把原始请求参数直接写进 SQL(比如记录“操作语句”字段),也得走白名单或长度截断+正则过滤
转义还能用在哪儿?仅限非参数化场景的兜底防护
极少数无法参数化的遗留代码(比如某些 ORM 的原生查询接口、动态视图生成脚本),才考虑转义,且必须匹配具体数据库驱动的官方转义函数。
- MySQL:用
mysqli_real_escape_string($conn, $input),而非addslashes() - PostgreSQL:用
pg_escape_string($conn, $input),注意它不处理数组或 JSON 类型 - 永远配合
SET NAMES utf8mb4确保连接层编码一致,避免宽字节绕过 - 对数字型参数,强制用
(int)$input或filter_var($input, FILTER_VALIDATE_INT),比转义更可靠
真实项目里最常翻车的,是以为“用了 htmlspecialchars() 就安全了”——那只是防 XSS,跟 SQL 注入完全无关;还有人把参数化当成银弹,却在 ORDER BY 后面硬塞用户输入。防住注入的关键,从来不是加多少层转义,而是守住“SQL 结构不可变”这条线。










