必须用sp_executesql而非EXEC,因其参数绑定使值不参与语法解析,彻底隔离代码与数据;表名列名等标识符须白名单校验+QUOTENAME()封装,严禁拼接;权限须最小化,防范DEFINER提权风险。

动态SQL必须用sp_executesql,不能用EXEC
SQL Server里EXEC(@sql)会把整个字符串当命令执行,用户输入一旦混入,立刻触发任意语句执行。比如传入@id = '1; DROP TABLE users; --',EXEC就照单全收。sp_executesql才是唯一合规路径——它支持参数绑定,值不参与语法解析,天然隔离代码与数据。
关键点:
-
@sql字符串里只能出现占位符(如@status),不能出现任何用户输入的字面量 - 第二个参数是类型声明字符串,必须显式写全:
N'@status TINYINT, @since DATETIME',不能只写N'@status' - 第三个及之后的参数必须按声明顺序传值:
@status = 1, @since = '2024-01-01',不能只传1, '2024-01-01'
表名、列名、排序字段这些标识符无法参数化,必须白名单校验
SQL Server不支持@table_name这种占位符用于FROM或ORDER BY位置。你写SELECT * FROM ' + @table_name,哪怕加了QUOTENAME()也挡不住]; DROP TABLE x; --这类结尾注入。
真正安全的做法只有两个:
- 硬编码映射:
CASE @sort_col WHEN 'name' THEN 'user_name' WHEN 'email' THEN 'contact_email' ELSE THROW 50000, 'Invalid sort column', 1 END - 运行时查
INFORMATION_SCHEMA:IF NOT EXISTS (SELECT 1 FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = @table_name AND TABLE_SCHEMA = 'dbo') THROW 50000, 'Table not allowed', 1
别信“过滤单引号”或“正则删分号”——这些在SQL Server存储过程中根本无效。
MySQL和PostgreSQL的动态SQL更难防,必须严格分离结构与值
MySQL的PREPARE + EXECUTE只支持?占位符绑值,不支持标识符;PostgreSQL的EXECUTE虽有quote_ident()防标识符注入,但对值仍需走USING。
错误示范:
- MySQL:
SET @sql = CONCAT('SELECT * FROM ', @table_name, ' WHERE id = ', @id)——@table_name和@id都危险 - PostgreSQL:
EXECUTE 'SELECT * FROM ' || quote_ident($1) || ' WHERE role = ''' || $2 || ''''——$2没走USING,照样被注入
正确做法:
- MySQL:先查
INFORMATION_SCHEMA.TABLES确认@table_name合法,再PREPARE stmt FROM 'SELECT * FROM t WHERE id = ?'; EXECUTE stmt USING @id - PostgreSQL:
EXECUTE 'SELECT * FROM users WHERE role = $1' USING user_role,表名用quote_ident()且仅限白名单来源
权限最小化比代码逻辑更重要,DEFINER权限常被忽略
即使存储过程写得滴水不漏,如果调用账号有FILE、SUPER或PROCESS权限,攻击者仍可能通过注入触发提权链。最常被忽略的一点是:存储过程默认以DEFINER权限执行,会继承创建者的全部权限。
必须检查并限制:
- 业务账号已执行
REVOKE FILE ON *.* FROM 'app_user'@'%',并用SHOW GRANTS验证 -
secure_file_priv设为受限目录(如/var/lib/mysql-files/),local_infile为OFF - 业务账号无
EXECUTE权限访问系统函数(如sys_exec),若有UDF加载记录需立即DROP FUNCTION
动态SQL的安全边界不在语法层,而在执行上下文——账号能做什么,远比代码写了什么更关键。











