必须显式写EXECUTE AS,否则默认EXECUTE AS CALLER导致权限穿透;SQL Server需用OWNER或专用低权限账户隔离,MySQL/PostgreSQL须显式设INVOKER并严格授权。

SQL Server 必须显式写 EXECUTE AS,否则默认是 CALLER
SQL Server 存储过程不写 EXECUTE AS,就等于没设防。它默认以 EXECUTE AS CALLER 运行,调用者权限直接穿透到过程内部所有语句——查 sys.tables、拼接动态 SQL、跨 schema 访问表,全都能干,只要调用者自己有权限。
常见现象:The SELECT permission was denied on the object 'sys.tables' 报错,其实是你没关掉 CALLER 模式,又没给用户授元数据权限;更隐蔽的问题是,用户能通过过程执行 EXEC(@sql) 去跑任意语句,只要他本身有 ALTER ANY DATABASE 就可能提权。
-
EXECUTE AS OWNER是最稳妥的起点:过程始终以创建者(如dbo)身份运行,调用者只需EXECUTE权限,无需底层表的任何SELECT/INSERT - 若需更细粒度控制,可用
EXECUTE AS USER = 'readonly_user',但该用户必须真实存在、不能是sysadmin成员、且对所有业务表只授SELECT - 每段
EXECUTE AS后必须配REVERT,否则后续语句会沿用错误上下文,造成权限泄漏
MySQL / PostgreSQL 的 SECURITY DEFINER 和 INVOKER 是开关,不是可选项
MySQL 和 PostgreSQL 不靠角色或 GRANT 控制过程内行为,而是靠定义时的 SQL SECURITY 设置。不改这个,其他授权全是摆设。
典型错误:过程定义为 DEFINER = 'root'@'localhost',用户哪怕只有 EXECUTE 权限,也能借过程读写所有表——因为执行时用的是 root 权限,完全绕过调用者权限检查。
- MySQL 8.0+ 要安全,必须显式声明
SQL SECURITY INVOKER;否则默认是DEFINER,且无法被GRANT覆盖 - PostgreSQL 必须确认函数定义中无
SECURITY DEFINER,否则会以定义者身份运行;若要用会话变量(如current_setting('app.tenant_id')),则必须用SECURITY DEFINER,但定义者账号权限要严格限制 - 验证命令:
SELECT ROUTINE_NAME, SECURITY_TYPE FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = 'your_db'(MySQL);PostgreSQL 查pg_proc.prosecdef字段
GRANT EXECUTE 只放行“调用动作”,不约束过程内部行为
GRANT EXECUTE ON PROCEDURE 或 GRANT EXECUTE ON OBJECT::usp_GetReport 都只管“能不能 CALL”,不管“CALL 之后干了啥”。用户能否真正只读,取决于过程里访问的所有对象的实际权限。
常见踩坑:给用户授了 EXECUTE,又踢出了 db_datawriter 角色,结果发现他还能 UPDATE 表——大概率是因为过程里用了 EXECUTE AS OWNER,而 OWNER(比如 dbo)有写权限;或者过程依赖的视图/函数本身没做权限隔离。
- 必须递归检查过程调用链上所有对象:基表、视图、标量函数、表值函数,确认用户对它们的
INSERT/UPDATE/DELETE权限是DENY或未授予 - SQL Server 中,
db_datawriter角色优先级高于单个对象的DENY,必须执行ALTER ROLE db_datawriter DROP MEMBER [app_user],不能只靠REVOKE - MySQL 中,即使
SQL SECURITY INVOKER,过程内 SQL 仍需调用者具备对应表的SELECT/INSERT权限;缺哪项,CALL就报EXECUTE command denied,而非更具体的错误
动态 SQL 是权限隔离的最大破口,必须单独加固
无论用哪种 EXECUTE AS 或 SQL SECURITY,一旦过程里出现 EXEC(@sql) 或 PREPARE,权限模型就基本失效——动态语句的执行上下文由当前会话决定,不再受过程定义约束。
尤其在 MySQL 中,PREPARE 语句在连接级生效,连接池归还后若未 DEALLOCATE,可能被下一个租户复用旧语句;SQL Server 中,sp_executesql 默认继承 caller 权限,哪怕外层写了 EXECUTE AS OWNER。
- 避免拼接 SQL:用参数化查询替代字符串拼接,例如 MySQL 中
SET @sql = 'SELECT * FROM orders WHERE tenant_id = ?';,再用EXECUTE stmt USING p_tenant_id; - SQL Server 动态 SQL 若必须用,应在
EXECUTE AS上下文中显式切换,例如EXECUTE AS USER = 'safe_executor'; EXEC sp_executesql @sql; REVERT; - PostgreSQL 中,
SECURITY DEFINER函数内执行动态 SQL 时,会使用定义者权限,但定义者账号必须仅具最小必要权限,且禁用CREATE TEMP TABLE等高危能力
真正实现调用者权限隔离,不是加一行 GRANT EXECUTE 就完事。它要求你同时控制三件事:过程定义时的执行上下文(EXECUTE AS / SQL SECURITY)、调用者的实际对象权限(逐层检查)、以及过程内是否引入动态 SQL 这种不可控路径。漏掉任何一环,都可能让权限设计形同虚设。

















