存储过程参数必须直接使用,严禁拼接进动态SQL;安全做法是参数直引用或通过USING绑定值,结构部分(如表名)须经元数据白名单校验,且执行账号权限需严格限制。
MySQL 存储过程中,只要参数不参与字符串拼接,就天然免疫注入。比如 SELECT * FROM users WHERE id = p_user_id 这种写法,p_user_id 是输入参数,MySQL 会按类型安全绑定,无需额外处理。
常见错误是把参数拿去拼 CONCAT():SET @sql = CONCAT('SELECT * FROM users WHERE id = ', p_user_id)——哪怕 p_user_id 是整数,也打开了注入口。一旦传入 1 OR SLEEP(5),查询就变成全表扫描加延迟。
PREPARE + EXECUTE 包裹含参数的字符串,除非真需要动态结构INT 参数传入字母会报错)本身已是第一道过滤当确实要构建可变表名或字段列表(比如分表查询),PREPARE + EXECUTE 不可避免,但必须严格分离:结构部分(表名、列名)靠白名单或系统视图校验;数据部分(WHERE 值、LIMIT 数)全部通过 USING 绑定。
错误示例:SET @sql = CONCAT('SELECT * FROM ', p_table_name, ' WHERE status = ''', p_status, '''') —— 这里 p_table_name 和 p_status 都没防住。
SET @sql = CONCAT('SELECT * FROM ', (SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'mydb' AND TABLE_NAME = p_table_name LIMIT 1));
PREPARE stmt FROM @sql; EXECUTE stmt USING p_status;
USING 只支持值绑定,不支持标识符,所以 p_status 安全,而表名校验必须前置SQL Server 中 EXEC(@sql) 等价于把整个字符串当命令执行,用户输入一旦混入,立刻执行任意语句。而 sp_executesql 支持参数化,值不参与语法解析,是唯一合规路径。
典型翻车点:有人以为给 @sql 加了 QUOTENAME() 就安全,但若参数本身是拼出来的:N'@p nvarchar(50) = ''' + @user_input + '''',还是回到了字符串拼接的老路。
EXEC sp_executesql @sql, N'@name nvarchar(50)', @name = @user_input
@sql 字符串里只能出现占位符 @name,不能出现任何用户输入的字面量N'@name nvarchar(50)')必须精确匹配,否则可能触发隐式转换导致截断或失败connection.escapeId()(Node.js mysql2)或 QUOTENAME()(SQL Server)能转义反引号或方括号,但它们不验证对象是否存在、是否合法。攻击者传入 user`; DROP TABLE logs; --,escapeId() 会输出 `user`; DROP TABLE logs; --`,依然执行两句话。
真正可靠的方式是查元数据表确认存在且归属预期范围:
SELECT 1 FROM information_schema.TABLES WHERE TABLE_SCHEMA = 'app_db' AND TABLE_NAME = p_table_name
IF NOT EXISTS (SELECT 1 FROM sys.tables t JOIN sys.schemas s ON t.schema_id = s.schema_id WHERE s.name = 'dbo' AND t.name = @table_name) THROW 50000, 'Invalid table', 1;
OBJECT_ID(@name) 单独判断——它对非法名称返回 NULL,但无法区分不存在和恶意截断最易被忽略的是权限上下文:即使表名校验通过,执行账号也必须无权访问其他敏感表。否则,校验形同虚设。