必须显式声明EXECUTE AS,否则默认EXECUTE AS CALLER导致调用者权限穿透;推荐WITH EXECUTE AS OWNER或专用账户如'proc_executor',禁用SELF,确保所有权链完整且动态SQL经白名单校验。

必须显式声明 EXECUTE AS,否则等于没设防
SQL Server 存储过程默认以 EXECUTE AS CALLER 运行,调用者权限直接穿透到内部所有语句。哪怕你只给了用户 EXECUTE 权限,只要过程里写了 SELECT * FROM sys.tables,用户就能看到元数据;写了 EXEC(@sql),还能拼出任意表名执行——这不是漏洞,是默认行为。
安全写法必须显式指定上下文:
-
WITH EXECUTE AS OWNER:最常用,过程始终以创建者(如dbo)身份运行,调用者只需EXECUTE权限,无需底层表权限 -
WITH EXECUTE AS 'proc_executor':推荐用于最小权限场景,该账户需提前建好、禁用登录、只授过程内必需的字段级权限(如仅UPDATE status) - 绝对避免
EXECUTE AS SELF:它把创建时的 login 名硬编码进过程,DBA 换人后 owner 变更会导致过程执行失败
所有权链断裂会瞬间暴露调用者权限
即使加了 EXECUTE AS OWNER,如果过程里访问的对象(表、视图、其他过程)不属于同一 owner,SQL Server 会中断所有权链,回落到调用者权限检查。比如过程在 dbo 下,但查了 sales.Customers(owner 是 sales),就会报错或越权。
确保所有权链完整的实操要点:
- 所有被访问对象(包括跨 schema 引用)必须属于同一 schema,推荐统一用
dbo - 若必须跨 schema,显式用
GRANT SELECT ON sales.Customers TO dbo并确认salesschema 的 owner 与过程 owner 一致 - 过程里调用的其他存储过程,也必须同样声明
EXECUTE AS,否则调用链一断,权限就回退到 caller
动态 SQL 是最大风险点,不能靠 EXECUTE AS 一劳永逸
EXEC(@sql) 或 sp_executesql 在 EXECUTE AS OWNER 下仍以 owner 权限执行,等于把 owner 的全部能力借给调用者。如果过程里拼接了用户输入的表名或字段名,就等同于开放了一个高权限 shell。
安全处理动态 SQL 的底线做法:
- 禁止直接拼接用户输入:
@table_name不能直接放进@sql = 'SELECT * FROM ' + @table_name - 改用白名单校验:
IF @table_name NOT IN ('Orders', 'Customers') THROW 50000, 'Invalid table', 1; - 用
sys.dm_exec_describe_first_result_set预检动态语句的元数据,确认返回列和类型符合预期(SQL Server 2012+) - 真需要灵活查询,改用参数化视图或行级安全策略(RLS),而不是放任动态 SQL
专用执行账户比 OWNER 更可控,但配置稍复杂
用 EXECUTE AS OWNER 简单,但风险是 owner(如 dbo)往往权限过大。更精细的做法是建专用账户 proc_executor,只给它过程内真正需要的权限,且禁止交互式登录。
创建和验证步骤:
- 在数据库中建用户:
CREATE USER proc_executor WITHOUT LOGIN; - 授最小权限:
GRANT UPDATE(status, updated_at) ON dbo.Orders TO proc_executor;(不是UPDATE ON dbo.Orders) - 过程声明:
CREATE PROCEDURE dbo.UpdateOrderStatus WITH EXECUTE AS 'proc_executor' AS - 验证:用低权限用户执行,确认它能改指定字段,但查不到
credit_card字段、删不了行、也看不到其他表
真正容易被忽略的是调用链中的“隐性回落”——比如这个过程里又调用了另一个没设 EXECUTE AS 的过程,那后半段就立刻回到 caller 权限。每个环节都得独立检查,不能只看入口。

















