本篇文章是SQL Server安全系列的第七篇,详细内容请参考原文。
Relational databases are used in an amazing variety of applications with connections from a dizzying array of clients over widely distributed networks,特别是互联网,使得数据几乎向任何人,任何地方开放。数据库可以包含相当大部分的人类知识,包括高度敏感的个人信息和关键数据。
数据库的这些特性使得有人想要窃取数据或通过篡改以损害它的主人。确保你的数据是安全的是一个关键部分来配置SQL Server和开发应用程序存储数据。这一系列将探索SQL Server 2012安全基础,那样你可以保护你的数据和服务器资源,getting as granular as you need to be to protect against the unique security threats that can affect your data.大部分的功能适用于早期版本的SQL Server,但我也会讨论只适用于SQL Server 2012和以后版本的功能。
大多数的时候,你可能会关心独立数据库中有单独所有者的数据和对象访问的安全。但有时需要从多个数据库中访问数据和对象,这会引起一些安全问题,增加数据访问的复杂性。在这一篇,你会学习跨数据库所有权链接,使你可以安全的跨越数据库边界。
所有权链接
大多数的时候,你创建的数据库对象所引用的其他对象都是在同一个数据库中。比如一个存储过程所访问的表在同一个数据库,一个视图所连接的表都在同一个数据库。但是,有时你需要创建对象访问其他数据库中的对象。在以前,跨数据库访问的安全规则遵循在单一数据库中的规则。访问用户需要对访问对象有必要的权限,连续的所有权链接允许SQL Server简化权限检查。毕竟,SQL Server不能放松警惕无论对象存储在哪。类似单一数据库中,所有权链接有助于简化跨数据库的安全管理。
所有权链接工作的基本原理是一样的,不管你是在一个单一的数据库还是跨数据库。所有数据库对象都有一个所有者,并且所有者控制谁在对象上有权限。对象访问其他对象,比如存储过程在SELECT语句把多张表联接起来,形成一个连续的所有权链接,只要一个所有者拥有所有对象。
这种情况下用户只要有顶层对象(比如存储过程、视图)的权限,访问其他对象并不需要对底层对象有权限,只要存在一个连续的所有权链接就行。SQL Server一旦证实用户对顶层对象有权限就会停止检查底层对象的权限。这种设计为用户提供了更多更好的控制权,因为用户只需要直接访问对象的权限。
提示:所有权链接只适用于对象的权限,如SELECT、UPDATE和EXECUTE操作。SQL Server总是检查数据定义语言语句的权限,因为这些权限应用于语句而不是对象。
跨数据库所有权链接
跨数据库所有权链接是所有权链接的一个扩充,所有权链接的所有对象(包括用户直接访问的对象以及底层相关的对象)在同一个数据库。唯一不同的是跨数据库所有权链接跨越数据库边界。因此你可以在数据库中有一个视图将多个数据库下的表联接在一起。或者是一个存储过程访问多个数据库下的对象。在这些情况下,用户直接访问的源对象依赖于另一个数据库中的对象。
这两种所有权链的唯一重要的区别是,主体可以跨库成为其他对象的所有者。数据库用户也是一个主体,是完全包含在一个单一数据库中。即使多个数据库中都具有相同名称的数据库用户,这些都是独立的,不同的主体,不能参与一个完整的所有权链接,除非所有这些用户都映射到相同的服务器级别登录名。所以相关的所有者是登录名,而不是数据库用户。一个关键的概念:一个连续的所有权链接中的对象的共同拥有者是一个服务器级的主体,而不是一个数据库级的主体。
SQL Server内部通过SID而不是名称来区别对象的所有者。在一个单一的数据库,all objects owned by a single user have a single SID specified as the owner,因为在同一数据库中用户名不能重复。但在跨数据库中,SID是终极所有者在服务器级别的登录名对应。不同的用户在不同的数据库下可以有不同的登录名,所以会有不同的SID。这可能是用户与登录名潜在容易混乱的方面,所以要确保你清楚这一点!
一个连续的所有权链接要求所有对象(源对象和关联对象)的所有者映射到相同的登录名及相同的SID。
探索跨数据库所有权链接
本篇的样例代码会说明如何使用跨数据库所有权链接并解释它的特性。代码首先会在服务器级别创建一个叫做SharedLogin登录名。之后的代码会使用此登录名作为所有权链接中对象的共享所有者。
USE master;
GO
IF SUSER_SID('SharedLogin') IS NOT NULL DROP LOGIN SharedLogin;
CREATE LOGIN SharedLogin WITH password = 'Y&2!@37z#F!l1zB';
GO
代码7.1 创建SharedLogin登录名
我们需要两个数据库来演示跨数据库所有权链接,因此接下来代码会创建它们。用户直接访问对象的数据库叫做SourceDB,关联对象的数据库叫做ChainedDB。ChainedDB包含一个dbo.AlaskaCity表,里面有Alaska三大城市的人口数据。代码7.2会创建ChainedDB库和AlaskaCity表,然后往表中插入部分数据:
IF DB_ID('ChainedDB') IS NOT NULL DROP DATABASE ChainedDB;
CREATE DATABASE ChainedDB;
GO
USE ChainedDB;
GO
-- Create a table for access from another database
CREATE TABLE dbo.AlaskaCity
(
AlaskaCityID INT NOT NULL IDENTITY(1, 1),
CityName NVARCHAR(20) NOT NULL,
Population INT NOT NULL
CONSTRAINT PK_AlaskaCity PRIMARY KEY (AlaskaCityID)
);
GO
INSERT INTO dbo.AlaskaCity (CityName, Population)
VALUES ('Fairbanks', 31535), ('Anchorage', 291826), ('Juneau', 31275);
GO
代码7.2 创建ChainedDB库和AlaskaCity表并插入测试数据
代码7.3创建SourceDB库和一个视图,用户直接访问视图以返回ChainedDB库AlaskaCity表中的数据。代码中包含一个查询语句测试视图返回数据是否正确。如果你是以sysadmin身份登录这个语句应该正常执行。作为管理员运行是很方便,但是在生产使用它不安全,通常一个用户访问的代码不会有所有对象的所有权!
IF DB_ID('SourceDB') IS NOT NULL DROP DATABASE SourceDB;
CREATE DATABASE SourceDB;
GO
USE SourceDB;
GO
-- Create a view that accesses ChainedDB.dbo.AlaskaCity
CREATE VIEW dbo.AlaskaCitiesView AS
SELECT * FROM ChainedDB.dbo.AlaskaCity;
GO
SELECT * FROM dbo.AlaskaCitiesView ORDER BY Population DESC;
代码7.3 创建SourceDB库和AlaskaCitiesView视图
现在你有两个数据库,其中一个库下的对象会访问另一库下的对象,而且在sysadmin下执行是正常。代码7.4在SourceDB库创建一个SourceUser用户映射到SharedLogin登录名,并且将AlaskaCitiesView视图的查询权限授予给SourceUser用户。然后代码更改执行上下文到SharedLogin并尝试访问AlaskaCitiesView视图。查询会成功吗?
USE SourceDB;
GO
-- Create a user in the SourceDB who will access the view
CREATE USER SourceUser FOR LOGIN SharedLogin;
GRANT SELECT ON dbo.AlaskaCitiesView TO SourceUser;
GO
-- Try accessing the view as SourceUser
EXECUTE AS LOGIN = 'SharedLogin';
SELECT * FROM dbo.AlaskaCitiesView ORDER BY Population DESC;
GO
REVERT;
代码7.4 创建一个SourceUser用户映射到SharedLogin登录名
查询抛出一个错误:服务器主体 "SharedLogin" 无法在当前安全上下文下访问数据库 "ChainedDB"。
我们有一个连续的所有权链接:视图和表共享相同的所有者——dbo数据库用户。但是在服务器实例或新数据库上没有启用跨数据库所有权链接,下面我们将启用它。
启用跨数据库所有权链接
新安装的SQL Server实例跨数据库所有权链接选项默认是关闭的。这是因为启用这个选项会在实例的安全盔甲上打开几个漏洞;我将在这篇后面简要地讲解风险。当选项是禁用的,当代码依赖跨数据库所有权链接就会生成权限拒绝错误。(正如你稍后会看到的,在代码执行前这不是唯一需要修正的问题,但它是第一个我们将解决。)
服务器级别启用
在服务器级别为所有数据库启用跨数据库所有权链接。如果你启用它,它会应用到所有数据库上而且你不能在数据库上限制它。
使用SSMS,对象资源管理器下右击实例->属性->安全性,在对话框的底部你会看到跨数据库所有权链接的选项,如图7.1所示,点击确定实例下的所有数据库就会启用。
图7.1 启用跨数据库所有权链接
你也可以用T-SQL语句启用,如代码7.5所示。比如在其他服务器实例将"cross db ownership chaining "值设为1启用,O禁用。确保使用RECONFIGURE语句而不需要重启实例才能让更改生效。一旦你执行了这段代码,你就可以在SQL Server实例的任何数据库中使用跨数据库所有权链接。
USE master;
GO
EXECUTE sp_configure 'cross db ownership chaining', 1;
RECONFIGURE;
代码7.5 使用T-SQL启用跨数据库所有权链接
如果你使用SSMS或T-SQL已启用了跨数据库所有权链接,请将它关闭。这不是启用它的最好方法,除非你真的打算在实例的每个数据库下从源对象访问关联对象。如果不打算这样,在实例上启用太不安全,而且不是好主意。
作为替代,你应该针对需要的数据库启用跨数据库所有权链接。
数据库级别启用
在服务器级别禁用了跨数据库所有权链接,如果你需要使用的话,你必须在数据库级别启用它。如果一个数据库禁用了,它就不能作为源或链接数据库参与跨数据库所有权链接。为了让链接工作,在源和链接数据库都必须设置启用。
默认当你创建或附加用户数据库时跨数据库所有权链接是禁用的。但是master、msdb和tempdb系统数据库是启用的,model数据库是禁用的。因为SQL Server用户内部使用跨数据库所有权链接,我们不能禁用或启用系统数据的选项。
提示:如果你分离然后再附加一个已经启用跨数据库所有权链接的数据库,你必须在数据库附加后重新启用选项。这不适合于服务器级别已启用的,服务器级别会自动应用到所有数据库上。
不幸地是,在SSMS->数据库属性->选项,"跨数据库所有权链接已启用"选项是False,如图7.2所示。为了更改数据库的这个值,你需要使用代码7.6,启用SourceDB和ChainedDB数据库的设置。
图7.2 查看ChainedDB数据库跨数据库所有权链接已启用选项
ALTER DATABASE ChainedDB SET DB_CHAINING ON;
ALTER DATABASE SourceDB SET DB_CHAINING ON;
代码7.6 启用跨数据库所有权链接
两个数据库都启用跨数据库所有权链接后,你可以尝试使用SharedLogin安全上下文再次访问视图:
USE SourceDB;
GO
EXECUTE AS LOGIN = 'SharedLogin';
SELECT * FROM dbo.AlaskaCitiesView ORDER BY Population DESC;
GO
REVERT;
代码7.7 尝试使用跨数据库所有权链接跨过数据库边界访问对象
还是抛出同样的错误:服务器主体 "SharedLogin" 无法在当前安全上下文下访问数据库 "ChainedDB"。这次的问题是要满足一个需求。用户访问视图必须也能够访问ChainedDB数据库,用户不需底层对象的权限。事实上,用户不需要在那个数据库下的任何权限。
代码7.8展示了如何解决这个问题。在ChainedDB库创建一个ChainedUser用户映射到SharedLogin登录名。这给SharedLogin在ChainedDB库一个立足点,跨数据库链接工作的必要条件。为了验证SharedLogin在AlaskaCity表上没有查询权限,代码尝试直接从AlaskaCity表上读取数据。但是尝试失败,因为SharedLogin和ChainedUser在ChainedDB库没有任何权限。这次的错误是:拒绝了对对象 'AlaskaCity' (数据库 'ChainedDB',架构 'dbo')的 SELECT 权限。
USE ChainedDB;
GO
CREATE USER ChainedUser FOR LOGIN SharedLogin;
-- Note that we're not granting the user any permissions in the chained database.
GO
-- Verify that SharedLogin doesn't have direct access to the AlaskaCity table, even in the ChainedDB database context.
EXECUTE AS LOGIN = 'SharedLogin';
SELECT * FROM dbo.AlaskaCity;
GO
REVERT;
代码7.8 ChainedDB库创建一个ChainedUser用户映射到SharedLogin登录名
现在你可以返回执行代码7.7,它会成功并返回视图中的数据!
使用代码7.9禁用数据库所有权链接——实际上你只需要执行代码中任何一条语句,因为要让跨数据库所有权链接正常工作两个数据库的都要设置成ON。然后你再运行代码7.7,此时对象所有者相同且用户能访问两个数据库。这次报错:拒绝了对对象 'AlaskaCity' (数据库 'ChainedDB',架构 'dbo')的 SELECT 权限。
ALTER DATABASE ChainedDB SET DB_CHAINING OFF;
ALTER DATABASE SourceDB SET DB_CHAINING OFF;
代码7.9 禁用数据库所有权链接
共用所有权
样例代码中的视图在启用跨数据库所有权链接后能正常执行的原因是:视图和表共用所有权。样例代码假定是以sysadmin登录到SSMS编写的,因此代码所创建的对象都被dbo用户所拥有,用户都是映射到sysadmin。你可以用代码7.10验证映射关系,显示架构名称、所有者用户名、所有者登录名。如果你在SourceDB和ChainedDB库下运行,你会发现所有者登录名是相同的,如图7.3和7.4所示,在我的例子中,dbo用户映射到登录名USER-67NP5R8LGK\Administrator.
SELECT
so.[name] AS Object,
sc.[name] AS [Schema],
USER_NAME(COALESCE(so.principal_id, sc.principal_id)) AS OwnerUserName,
sp.name AS OwnerLoginName,
so.type_desc AS ObjectType
FROM sys.objects so
JOIN sys.schemas sc ON so.schema_id = sc.schema_id
JOIN sys.database_principals dp ON dp.principal_id = COALESCE(so.principal_id, sc.principal_id)
LEFT JOIN master.sys.server_principals sp ON dp.sid = sp.sid
WHERE so.[type] IN ('U', 'V');
代码7.10 显示对象架构名称、所有者用户名、所有者登录名
图7.3 SourceDB库下运行结果
图7.4 ChainedDB库下运行结果
要注意的是一个数据库的dbo用户不一定和另一个数据库下的dbo用户相同。它们可以被映射到完全不同的登录名或者不映射。重要的是链接中所有对象的拥有者有相同的SID。你可以用代码7.11验证对象所有者和登录名有相同的SID。
SELECT name, sid FROM SourceDB.sys.database_principals WHERE name = 'dbo'
UNION ALL
SELECT name, sid FROM ChainedDB.sys.database_principals WHERE name = 'dbo'
UNION ALL
SELECT 'USER-67NP5R8LGK\Administrator', SUSER_SID('USER-67NP5R8LGK\Administrator');
代码7.11 验证对象所有者和登录名有相同的SID
图7.5 SourceDB和ChainedDB库下dbo用户,及代码会话登录用户的SID
跨数据库所有权链接的风险
启用跨数据库所有权链接存在风险因素的原因是高特权用户可能滥用。微软以2种方式来描述风险,这两者都涉及到数据库的安全边界的潜在风险:
->在一个数据库中,数据库所有者和db_ddladmin和db_owners数据库角色的成员可以创建其他用户所拥有的对象。这些新对象能够使用其他数据库下具有相同所有者的对象,这样无意中就可以访问其他数据库下的对象。
->具有CREATE DATABASE权限的数据库用户可以创建新数据库和附加数据库。启用跨数据库所有权链接,这些用户可以从新创建的或附加的数据库访问其他数据库对象。
这里的关键是,你要相信高特权用户,那些权限超越了简单的访问和在特定表的维护数据,如果你要允许跨数据库所有权链接。这就是为什么微软强烈建议,如果你需要启用跨数据库所有权链接,只在需要的数据库上启用它,为了遏制安全风险。
-- Clean up
USE master;
GO
IF SUSER_SID('SharedLogin') IS NOT NULL DROP LOGIN SharedLogin;
IF DB_ID('ChainedDB') IS NOT NULL DROP DATABASE ChainedDB;
IF DB_ID('SourceDB') IS NOT NULL DROP DATABASE SourceDB;
总结
跨数据库所有权链接是SQL Server帮助保持你的数据安全的另一种方式。禁用这个选项,恶意用户访问其他数据库中的数据就会更难。但在正确的场景和安全环境中,你可以启用此选项让数据库对象所有者对其数据有更严格的控制。你应该很少在服务器级别启用跨数据库所有权链接。相反,只为你真正需要的数据库启用,并确保不被特权用户滥用。