必须用预编译语句替代所有SQL字符串拼接,用户输入只能作参数绑定,严禁参与SQL构造;各语言需严格遵循参数化规范,警惕伪安全变体、存储过程内拼接及模糊搜索通配符注入。

直接用预编译语句替代字符串拼接
所有动态拼接 SELECT、INSERT、UPDATE、DELETE 的地方,必须改用参数化查询。这不是“建议”,而是硬性边界——只要还存在 "WHERE id = " + userId 这类写法,漏洞就还在。
不同语言的实现差异大,但核心原则一致:用户输入只能作为参数传入,不能参与 SQL 字符串构造。
- Java(JDBC):用
PreparedStatement,调用setString()、setInt()等方法绑定值,绝不用Statement.execute() - Python(sqlite3 / psycopg2):用
?(SQLite)或%s(PostgreSQL)占位符,通过cursor.execute(sql, (val1, val2))传参 - PHP(PDO):必须设
PDO::ATTR_EMULATE_PREPARES = false,否则 MySQL 驱动可能退化为字符串拼接模拟预编译 - Node.js(pg / mysql2):用
$1、$2(PostgreSQL)或?(MySQL)占位符,配合query(sql, params)调用
警惕看似安全的“拼接”变体
有些代码看起来没直接拼 SQL,实则仍危险。比如把用户输入先做 parseInt() 再拼进 WHERE 条件,或用模板字符串包一层再执行——这些都不算防护,只是障眼法。
常见误判场景:
-
id = ${parseInt(req.query.id)}:整数转换不防 SQL 注入,因为攻击者可构造id=1 OR 1=1,parseInt("1 OR 1=1")仍得1,后续若又拼回字符串就崩了 -
sql = `SELECT * FROM users WHERE name = '${escapeSql(name)}'`:自写escapeSql几乎必然漏场景(如 MySQL 的宽字节、反斜杠转义绕过),且无法覆盖所有数据库方言 -
knex('users').where('name', req.query.name):Knex 等 ORM 默认参数化,但若混用.whereRaw()或.raw()并传入用户输入,立刻失效
存储过程里也得查参数化逻辑
很多人以为“用了存储过程就天然免疫”,其实错在过程内部是否拼接。如果存储过程中有 EXEC('SELECT * FROM ' + @table_name) 或 sp_executesql 拼接动态表名/字段名,照样中招。
修复要点:
- 禁止在存储过程中拼接表名、列名、排序字段等结构信息;必须动态时,用白名单校验(如
@sort_col IN ('created_at', 'status')) - 所有传入值必须用参数方式绑定,例如 SQL Server 中
sp_executesql @sql, N'@id INT', @id = @user_id - 避免使用
EXEC(@sql),它不支持参数化,也无法被查询计划缓存复用
搜索型注入的特殊处理路径
模糊搜索(LIKE '%xxx%')是重灾区。直接把用户输入塞进 CONCAT('%', ?, '%') 不够,因为通配符 % 和 _ 本身会被数据库当作模式字符解析。
正确做法:
- 对用户输入中的
%、_、\做转义,并在 SQL 中声明 ESCAPE 字符,例如:WHERE name LIKE CONCAT('%', REPLACE(REPLACE(?, '%', '\%'), '_', '\_'), '%') ESCAPE '\' - 更稳妥的是交由数据库函数处理,如 PostgreSQL 的
ESCAPE子句配合ESCAPE参数,或 MySQL 8.0+ 的REGEXP_LIKE替代部分模糊需求 - 绝不允许前端传入原始
LIKE模式(如q=%admin%),这种等于把通配符控制权直接交给攻击者
重构拼接 SQL 最容易被忽略的点,是那些藏在日志记录、调试输出、缓存键生成里的隐式拼接——它们不走主业务查询路径,但一旦被攻击者诱导触发(比如通过异常堆栈泄露或缓存投毒),就能成为注入跳板。每处字符串拼接,都得问一句:这个变量是否完全可控?是否经过参数化通道?

















