存储过程内使用EXEC拼接动态SQL等于裸奔,因SQL Server不自动参数化,表名列名等无法参数化,必须用白名单校验;二次注入和权限最小化同样关键。 存储过程里用 EXEC 拼字符串等于裸奔 这里有个常见的误解,以为只要把逻辑封装进存储过程,就等于进了保险箱。但事实是,只要存储过程内部出现了 E

EXEC 拼字符串等于裸奔这里有个常见的误解,以为只要把逻辑封装进存储过程,就等于进了保险箱。但事实是,只要存储过程内部出现了 EXEC(@sql) 或 EXEC sp_executesql @sql,并且那个 @sql 是拼接出来的,那么它的安全风险就和在应用层直接拼接用户输入没什么两样。SQL Server 可不会因为你在 CREATE PROCEDURE 里写代码,就自动帮你做过滤或参数化处理。
长期稳定更新的攒劲资源: >>>点此立即查看<<<
来看一个典型的错误模式:
DECLARE @sql NVARCHAR(MAX) = 'SELECT * FROM users WHERE id = ' + CAST(@id AS VARCHAR(10));
EXEC(@sql);
这段代码里,@id 虽然是 INT 类型,但经过 CAST 转换成字符串再拼进 SQL,隐患就埋下了。攻击者甚至不需要传入一个恶意的字符串——如果上游调用时,给 @id 传入类似 1 OR 1=1 的值,SQL Server 在隐式转换阶段就可能报错或产生非预期行为。而真正危险的情况是:如果这个 @id 本身来自应用层一个未经验证的字符串参数(比如定义为 @id NVARCHAR(50)),那么拼接进去的就是赤裸裸的代码片段。
EXEC(@sql) 这种方式完全不支持参数绑定,应该彻底杜绝。sp_executesql,它的第二个参数(类型声明列表)也必须显式、完整地写出。例如,对于字符串参数,不能只写 N'@status INT' 而忽略长度,必须明确如 N'@status NVARCHAR(10)',这对字符串类型尤其关键。'WHERE id = ' + @id 而 @id 是 INT 类型,SQL Server 会直接报类型不匹配错误而停止执行。但这可不是什么安全机制,仅仅是编译失败而已,别把它当成防护手段。这是 SQL Server 语法的一个硬性限制:像 @table_name、@order_col 这类数据库对象名,无法作为参数占位符使用。只要硬拼,就是高危操作,没有任何例外可言。
看看下面这种常见的错误写法:
DECLARE @sql NVARCHAR(MAX) = 'SELECT * FROM ' + @table_name + ' ORDER BY ' + @order_col;
EXEC sp_executesql @sql;
无论你怎么套上 sp_executesql 的外壳,都拦不住注入。因为 @table_name 和 @order_col 是直接嵌入到 SQL 语句的语法结构里的,而不是作为参数值传递的。
#allowed_tables,里面存放所有允许访问的表名。在执行拼接前,先用 IF NOT EXISTS (SELECT 1 FROM #allowed_tables WHERE name = @table_name) RAISERROR(...) 这样的逻辑进行校验。REPLACE(@input, '''', '''''') 转义单引号,或者用正则表达式删除某些字符。绕过的方法太多了,而且很容易漏掉括号、方括号、点号这些在对象名里合法但组合起来却很危险的字符。“二次注入”这个概念值得再强调一遍。它指的是恶意数据第一次存入数据库时,看起来一切正常(比如用户在昵称字段里输入了 admin'; DROP TABLE users--)。等到后续某个存储过程读取这个字段的值,并将其拼接到新的 SQL 语句中执行时,攻击才真正被触发。必须清醒地认识到,存储过程并不是这个攻击链的免疫区。
一个典型的场景是这样的:
users.nickname 字段。SELECT nickname FROM users WHERE active = 1,读出了这个昵称。'UPDATE logs SET remark = ''' + @nickname + ''' WHERE ...'。@nickname 变量里包含的单引号和 SQL 代码片段就被完整地拼接并执行了。防御的要点在于:
REPLACE 或 QUOTENAME 处理后就直接拼接。QUOTENAME 函数设计是用来处理对象名(如表名、列名)的,对于普通的字符串值,它可能产生非预期结果。sp_executesql 传递进去,而不是将其拼接到 SQL 字符串内部。这是最后一道,也是至关重要的一道防线。即便你的每一个存储过程都写得滴水不漏,如果调用这些存储过程的数据库账户拥有过高的权限(比如 db_owner,或者对基础表拥有直接的 SELECT/INSERT 权限),攻击者依然可以绕过存储过程,直接操作表或探查结构。存储过程的安全模型很大程度上依赖于“所有权链”(ownership chaining),而这个机制生效的前提,恰恰是基础表的直接权限要从调用者身上收回。
REVOKE SELECT ON users FROM public 收回基础表的权限,然后再通过 GRANT EXECUTE ON Sp_GetUser TO app_user 授予执行特定存储过程的权限。EXECUTE AS 子句定义)与表的所有者保持一致。如果所有权链断裂,权限检查就会回退到调用者,最小化权限的设定就失效了。guest 用户,以防匿名连接获取到默认权限。最容易被团队忽略的一点就在于此:很多人以为“用了存储过程就等于防住了注入”,却忘了撤消应用程序账户对底层表的直接访问权限。在这种情况下,攻击者完全可能利用存储过程中的动态 SQL 执行类似 EXEC('SELECT * FROM sys.tables') 的语句来探测数据库结构,为后续更精准的攻击铺平道路。所以说,权限最小化不是锦上添花,而是存储过程安全体系得以成立的基石。
侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述