动态拼接SQL字符串必然导致SQL注入风险,因用户输入与SQL模板在应用层合并为完整语句,数据库无法区分代码与数据;参数化接口(如PreparedStatement、#{ })将结构与参数分离,从根本上阻断注入。

动态拼接 SQL 字符串必然导致 SQL 注入风险,没有例外。只要用户输入出现在 SELECT、WHERE、ORDER BY 等语句的字符串内部,且未经过数据库驱动原生参数化机制处理,攻击者就能用 ' OR '1'='1、); DROP TABLE users-- 这类输入篡改查询逻辑。
拼接 SQL 时数据库根本分不清哪是代码哪是数据
你写的 "SELECT * FROM users WHERE name = '" + username + "'",在应用层就合并成一条完整字符串,再发给数据库。数据库收到的是成品,不会重新解析语法结构——它只认最终字符串的语法,不关心“这段是不是用户填的”。比如 username = "admin' --" 会让实际执行变成:
SELECT * FROM users WHERE name = 'admin' --'
注释符后面的内容被忽略,条件直接失效。
- 所有拼接方式(
+、String.format()、f-string、StringBuilder.append()、MyBatis 的${})本质相同,都发生在应用内存中 - 哪怕做了
replaceAll("'", "''")或正则过滤,也极易被绕过(如用/**/、URL 编码、大小写混用) - MySQL 开启
allowMultiQueries=true时,拼接还可能触发堆叠注入(一次请求执行多条语句)
PreparedStatement / ? 占位符 / #{} 是怎么防住的
参数化不是“加引号”或“转义”,而是让数据库驱动把 SQL 结构和参数彻底分离:
- 先传模板(如
"SELECT * FROM users WHERE id = ?")给数据库预编译,生成执行计划 - 再单独传参数值(如
setString(1, "123' OR '1'='1")),驱动按类型绑定,数据库明确知道“这个 ? 是字符串值,不是字段名,更不是子查询” - 即使参数含单引号、分号、
UNION,它也只被当作文本内容,不会参与语法解析
Node.js 的 mysql2.query("SELECT * FROM t WHERE id = ?", [id])、Python 的 cursor.execute("SELECT * FROM t WHERE name = ?", (name,))、Java 的 ps.setString(1, name),底层都是这一机制。
哪些地方看似能参数化,其实不能直接套用
参数化只适用于**数据值**,不适用于 SQL 结构部分(表名、字段名、排序方向、IN 子句占位符数量等)。这些必须走白名单或严格校验:
-
ORDER BY ?会报错,必须先校验sortField是否在['created_at', 'score', 'name']中 -
INSERT INTO ${table}没有安全占位符方案,只能用正则^[a-z_][a-z0-9_]{2,31}$或枚举匹配 -
WHERE id IN (?)传数组会失败,得动态拼WHERE id IN (?, ?, ?)再绑定三个值 -
WHERE name LIKE '%?%'是错的——?会被当字面量,应先在代码里拼好'%' + userInput + '%',再绑定
HQL、ORM、text() 查询不是免死金牌
用了 Hibernate、SQLAlchemy 或 MyBatis 不代表自动防注入。只要你在 HQL 里写 "from User u where u.name = '" + username + "'",或在 Flask 里写 text(f"SELECT * FROM users WHERE name = '{name}'"),Hibernate 和 SQLAlchemy 就收不到“参数”,只看到一个拼完的字符串,照样翻译执行。
必须用 setParameter("name", username)、session.execute(text("... :name"), {"name": name})、User.query.filter(User.name == name) 这类显式参数绑定方式。
最危险的其实是那些“看起来安全”的地方:动态排序、多表切换、模糊搜索、IN 列表——它们往往绕开了参数化惯性思维,又缺乏白名单兜底,是真实攻防中高频失守点。











