不能防注入,但能限制后果:execute as 仅控制执行身份,不阻止注入发生;需搭配显式权限控制、禁用 trustworthy、避免调用未隔离过程等措施才能守住边界。

EXECUTE AS 能不能防注入?不能,但能限制注入后果
EXECUTE AS 本身不防 SQL 注入,它只管“谁在执行”,不管“语句怎么来”。如果应用层拼接了恶意字符串,再传给 EXEC(@sql),注入已经发生;EXECUTE AS 的作用是让这个被拼出来的恶意语句,以一个低权限用户身份去跑——哪怕注入成功,也最多查到它被授权的那几张表。
常见错误现象:The SELECT permission was denied on object 'sys.dm_exec_sessions',说明你用了 EXECUTE AS 'readonly_user',但忘了给这个用户授 VIEW SERVER STATE 权限;或者更典型的是,注入语句试图 DROP TABLE,结果报错 The EXECUTE permission was denied——这正是你想要的效果。
- 必须搭配显式权限控制:
readonly_user不能属于任何高权限角色(如db_owner),且对所有业务表只授SELECT,对系统视图按需单独授VIEW DEFINITION或VIEW SERVER STATE - 避免用
EXECUTE AS OWNER:如果 owner 是dbo且dbo属于db_owner,那注入后就能干任何事 - 动态 SQL 必须在
EXECUTE AS切换后执行:写法是先EXECUTE AS USER = 'readonly_user',再EXEC(@sql),最后REVERT;不能把EXECUTE AS写进@sql字符串里(SQL Server 2016+ 直接报错Msg 3272)
为什么 EXECUTE AS USER 比 EXECUTE AS LOGIN 更适合防注入
防注入场景下,目标是把攻击面缩到最小——只要不让恶意语句跨出当前数据库、访问不该看的元数据或系统功能就行。EXECUTE AS USER 天然满足这点:它只在当前数据库生效,访问其他库(比如 master)或执行服务器级操作(如 SHUTDOWN)会直接失败,不需要额外配置。
而 EXECUTE AS LOGIN 会继承登录级别的权限,一旦该登录被授予 VIEW SERVER STATE 或 ALTER ANY DATABASE,注入后就能扫全实例会话、删任意库——这违背最小权限原则。
-
EXECUTE AS USER = 'app_reader'后,SELECT * FROM master.sys.tables直接报错,无需手动DENY -
EXECUTE AS LOGIN = 'svc_login'后,若该登录有sysadmin角色,EXEC('DROP DATABASE tempdb')真的能跑通 - 跨库查询(如
SELECT * FROM OtherDB.dbo.config)在USER模式下默认失败,除非你显式给app_reader授了OtherDB中对应对象的权限——这是可控的显式放行,不是默认开放
如何验证 EXECUTE AS 是否真正生效并守住边界
别只信 SELECT USER_NAME() 返回值。真实权限边界要看具体语句能否执行成功。最简验证方式是在存储过程里嵌入几条“探测语句”,用 TRY...CATCH 捕获实际拒绝行为:
EXECUTE AS USER = 'app_reader'; BEGIN TRY SELECT TOP 1 name FROM sys.tables; -- 应失败(没 VIEW DEFINITION) SELECT TOP 1 session_id FROM sys.dm_exec_sessions; -- 应失败(没 VIEW SERVER STATE) SELECT TOP 1 id FROM dbo.orders; -- 应成功(已授 SELECT) END TRY BEGIN CATCH SELECT ERROR_MESSAGE() AS msg; END CATCH REVERT;
- 测试重点不是“能不能切过去”,而是“切过去之后哪些操作被拦住”——这才是防注入的实际防线
- 注意
sys.tables默认需要VIEW DEFINITION,不是SELECT;很多开发者漏掉这步,导致过程一运行就崩,误以为EXECUTE AS失效 - 如果过程里用了
INSERT/UPDATE,而app_reader没被授对应权限,ERROR_NUMBER()会是229(权限拒绝),不是语法错误
容易被忽略的三个权限泄漏点
即使 EXECUTE AS USER 写对了,权限仍可能从侧面漏出去。这三个点不常被检查,但线上出过真实事故:
- 过程调用了另一个没设
EXECUTE AS的存储过程:被调用者默认走CALLER模式,权限回落到原始调用者,等于绕过你的隔离 - 过程里用了
OPENROWSET或OPENDATASOURCE:这些函数不受EXECUTE AS约束,会以服务账户身份运行,必须显式DENY或关闭Ad Hoc Distributed Queries配置 - 数据库启用了
TRUSTWORTHY ON:这会让EXECUTE AS用户能跨库访问未显式授权的对象,应始终设为OFF(默认值)
真正的防线不在一句 EXECUTE AS,而在它后面那一整套显式授权、显式拒绝、显式验证的闭环。少一步,边界就松动一分。











