最直接有效的防SQL注入手段是使用PreparedStatement(Java)、mysqli_prepare()/PDO::prepare()(PHP)、参数化查询(Python/Node.js)等预编译机制,将SQL结构与参数严格分离,杜绝字符串拼接。

用 PreparedStatement 替代字符串拼接
Java 里最直接有效的防注入手段,就是别用 Statement 拼 String,改用 PreparedStatement。它把 SQL 结构和参数分开处理,数据库驱动会自动转义或绑定参数,用户输什么都不会变成语句的一部分。
常见错误现象:String sql = "SELECT * FROM user WHERE name = '" + name + "'"; —— 只要 name 是 "' OR '1'='1",整条查询就失控了。
实操建议:
- SQL 中的变量位置统一用
?占位,比如"SELECT * FROM user WHERE id = ? AND status = ?" - 用
setString()、setInt()等方法逐个设值,不要自己拼类型转换 - 批量操作优先用
addBatch()+executeBatch(),避免循环里反复编译 SQL - 注意:
?只能替换**参数值**,不能替换表名、字段名、ORDER BY子句——这些必须走白名单校验
PHP 中必须用 mysqli_prepare() 或 PDO::prepare()
PHP 原生 mysql_*() 函数已废弃且不支持预编译,哪怕你写 mysql_real_escape_string() 也挡不住多字节编码绕过或二次注入。真正安全的只有带绑定参数的准备语句。
使用场景:所有含用户输入的 SELECT、INSERT、UPDATE、DELETE 都该走这路子。
实操建议:
- 用
PDO时开启PDO::ATTR_EMULATE_PREPARES = false,否则 PHP 会模拟预编译,失去防注入能力 -
mysqli下,mysqli_prepare()返回mysqli_stmt对象,必须用bind_param()绑定,不能直接传数组 - 参数类型标识符(
"s"、"i")要严格匹配,"s"不代表“字符串安全”,只是告诉驱动按字符串处理 - 错误信息如
"Commands out of sync"往往是因为没调mysqli_stmt::fetch()就又执行新语句,资源没清理干净
Python 的 sqlite3 和 psycopg2 怎么防注入
Python 标准库 sqlite3 默认启用参数化查询,但很多人误以为 %s 或 .format() 是安全的——其实只要用了字符串插值,就等于开了后门。
性能影响:预编译语句在连接池复用时会被缓存,首次执行略慢,后续极快;而拼接 SQL 每次都要解析、编译、生成执行计划,反而更耗资源。
实操建议:
-
sqlite3只认?或命名占位符(:name),绝不用%或{} -
psycopg2支持%s占位符,但它不是 Python 字符串格式化,而是驱动识别的绑定符号——所以必须用cursor.execute(sql, params)形式传元组或字典 - 传参时别用
str(user_input)包一层再塞进去,类型不对可能触发隐式转换漏洞 - 像
IN (?, ?, ?)这种动态长度场景,得手动拼占位符数量,再构造对应长度的参数元组
哪些地方看似安全,其实还在拼接 SQL
很多团队以为用了 ORM 就万事大吉,结果在 filter() 里写 name__contains=request.GET.get('q') 是安全的,但一写 extra(where=["name LIKE '%" + q + "%"]) 就破防。ORM 只保它自己生成的部分,不保你手写的 SQL 片段。
容易踩的坑:
- MyBatis 的
${}是字符串替换,#{}才是预编译——配置错一个符号就全白搭 - Node.js 的
pg模块,client.query("SELECT * FROM t WHERE id = $1", [id])安全,但client.query(`SELECT * FROM t WHERE id = ${id}`)直接裸奔 - 日志里打印完整 SQL 时,如果把绑定后的语句再拼成字符串输出,可能泄露敏感参数,也容易被日志注入利用
- 存储过程里用
EXEC(@sql)或EXECUTE IMMEDIATE,如果拼的是用户输入,照样中招
最麻烦的其实是那些“只读”接口——开发觉得查数据无所谓,结果攻击者用 UNION SELECT 把密码表拖出来。防注入不是看操作类型,是看有没有让输入参与语句结构构建。










