参数化INSERT是唯一可靠防线,必须用?占位符绑定用户输入,杜绝字符串拼接;任何过滤、转义手段均可能失效且易被绕过。

直接用参数化 INSERT 语句,别拼字符串 —— 这是唯一可靠防线。 所有其他“过滤关键字”“转义单引号”等手段,在复杂输入或不同数据库方言下都可能失效,且维护成本高、易被绕过。
为什么字符串拼接 INSERT 是高危操作
INSERT 注入和 SELECT 注入原理一致:只要把用户输入直接嵌入 SQL 字符串,攻击者就能用 ' 提前闭合字段值,再用 ; 或 -- 注入额外语句。比如:
INSERT INTO users (name, email) VALUES ('<strong>admin'</strong>, 'a@b.com');
若 name 来自用户且未处理,输入 admin', 'x@y.com'); DROP TABLE users; -- 就会执行删除表操作(取决于数据库是否支持多语句)。
常见错误场景包括:
- 用
String.format()或+拼接 INSERT SQL - MyBatis 中误用
${}替代#{},如INSERT INTO t VALUES('${name}') - PHP 中用
mysql_query("INSERT ... '$name'")(已废弃但仍有遗留)
Java + JDBC 必须用 PreparedStatement
这是最底层、最通用的防御方式,所有主流数据库驱动都支持。
正确写法要点:
- SQL 字符串中用
?占位,**不出现任何用户变量名或值** - 调用
setString()/setInt()等方法传参,由 JDBC 驱动完成类型安全绑定 - 避免使用
addBatch()时混入字符串拼接逻辑
示例:
String sql = "INSERT INTO users (username, email, age) VALUES (?, ?, ?)";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, userInputName); // 自动转义、类型校验
ps.setString(2, userInputEmail);
ps.setInt(3, userAge);
ps.executeUpdate();
}
MyBatis 中 INSERT 的安全写法
MyBatis 默认通过 #{} 实现预编译,但开发者常在模糊查询或动态字段场景误用 ${},INSERT 同样危险。
必须遵守:
- 所有用户输入字段一律用
#{username}、#{email},**禁用${username}** - 动态表名/列名(极少见且应避免)若真需,必须白名单校验,不能依赖用户输入
- 批量插入用
<foreach>时,内部仍为#{item.name},不是${item.name}
错误示例(高危):
<insert id="insertUser">
INSERT INTO users (username, email) VALUES ('${username}', '${email}')
</insert>
正确示例:
<insert id="insertUser">
INSERT INTO users (username, email) VALUES (#{username}, #{email})
</insert>
ORM 框架也不能掉以轻心
像 Hibernate、MyBatis-Plus 等框架封装了 INSERT,但安全前提仍是:你传进去的是原始值,不是拼好的 SQL 片段。
容易踩的坑:
- Hibernate 的
@Query原生 SQL 中用了:param却又手动拼接字符串 - MyBatis-Plus 的
QueryWrapper调用eq("name", "'" + input + "'")—— 这等于自己造了个注入点 - 用 LambdaQueryWrapper 时传入表达式而非纯值,如
eq(User::getName, "admin' OR 1=1--"),虽框架会转义,但逻辑上已暴露意图
核心原则没变:**只要最终生成的 SQL 包含未经绑定的用户输入文本,就存在风险。**
最常被忽略的一点:INSERT 的安全不只是“防删库”,更是防数据污染 —— 攻击者可能插入恶意脚本到昵称字段,后续渲染到前端造成 XSS;或伪造管理员邮箱触发重置流程。参数化不是银弹,但它是最基础、最不可妥协的起点。

















