真正可控的存储过程执行权必须配合底层数据权限隔离,需剥离db_datawriter角色、检查所有依赖对象权限、避免DEFINER绕过,并递归验证调用栈各层权限。

只授权用户执行存储过程,不等于“只给 EXECUTE 权限就完事”——常见错误是授了 EXECUTE 却忘了用户仍能直连表、调其他 SP、或通过角色继承写权限。真正可控的执行权,必须配合底层数据权限隔离。
GRANT EXECUTE 本身不能防写操作
SQL Server 不在存储过程代码里做读/写语义检查。哪怕你写的 SP 里有 UPDATE Orders,只要调用者有表级 UPDATE 权限,就会成功执行;反之,哪怕 SP 里没写任何 DML,只要用户被显式 DENY INSERT ON Orders,查视图时若该视图依赖 Orders,照样报错“拒绝了对对象 'Orders' 的 INSERT 权限”。
-
GRANT EXECUTE ON OBJECT::usp_GetReport TO [app_user]只放行调用动作,不约束 SP 内部行为 - 用户是否真能“只读”,取决于其对 SP 所访问**所有基表、视图、函数**的实际权限
- 必须检查
sys.database_permissions中该用户对相关对象的INSERT/UPDATE/DELETE是否为DENY或未授予
必须先剥离 db_datawriter 角色
很多 DBA 授完 EXECUTE 就以为安全了,结果发现用户还能改表——大概率是因为用户属于 db_datawriter 固定角色。这个角色自动授予当前数据库下所有用户表的写权限,优先级高于单个对象的 DENY。
- 执行
ALTER ROLE db_datawriter DROP MEMBER [app_user]是硬性前置步骤 - 别依赖
REVOKE:它只是撤回之前GRANT的权限,而db_datawriter是角色继承,必须显式踢出 - 检查方式:
SELECT dp.name, drm.role_principal_id FROM sys.database_role_members drm JOIN sys.database_principals dp ON drm.member_principal_id = dp.principal_id WHERE drm.role_principal_id = DATABASE_PRINCIPAL_ID('db_datawriter')
避免 DEFINER 权限绕过(MySQL / PostgreSQL)
SQL Server 没有 DEFINER 模式,但 MySQL 和 PostgreSQL 用户常踩这个坑:存储过程/函数以高权限账户定义,导致调用者间接获得越权能力。
- MySQL:检查
SECURITY_TYPE字段,确保是INVOKER而非DEFINER;若为DEFINER='root@%',立即重建为SQL SECURITY INVOKER - PostgreSQL:确认函数定义中无
SECURITY DEFINER,且未使用SET search_path TO admin_schema切换到高权限 schema - 验证命令:
SELECT ROUTINE_NAME, SECURITY_TYPE FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA = 'your_db'(MySQL)
跨对象依赖容易漏检
一个看似只读的 SP,可能查视图,视图又 JOIN 了用户有写权限的表;或者调用标量函数,函数内部执行了 INSERT。权限检查是递归穿透的,不是只看 SP 自身代码。
- 用
sys.dm_exec_describe_first_result_set查 SP 返回结构,再人工追溯所有引用对象 - 重点盯住
VIEW和FUNCTION:它们不像表那样有直观的权限列表,需逐个查sys.database_permissions对应的major_id - 测试时别只跑
EXEC usp_GetData,要抓实际执行计划,看是否触达了未授权的表(SELECT * FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle))
最易被忽略的是权限链深度——你控住了 SP,控住了它直接查的表,但没控住它调用的另一个函数所写的临时表,或没关掉 SQL Server 的 TRUSTWORTHY 数据库属性。只读不是配置一次就能一劳永逸的事,得从调用栈最底端开始一层层验权限落地点。

















