SQL Server Blog

Cross-Database Ownership Chaining in SQL Server: A Double-Edged Sword

This post is part of our SQL Server security blog series, 30 SQL Server Security Checks in 30 Days. We’re publishing a new security check every day in the month of June. Visit our sp_CheckSecurity page to learn about our free SQL Server tool you can download and run to check your own server.

Cross-database ownership chaining is a feature in Microsoft SQL Server that allows stored procedures, views, and triggers to access objects across databases. While it can be a powerful tool, it also has potential security implications if not used correctly.

What is Cross-Database Ownership Chaining?

In SQL Server, an ownership chain is created when one object (like a stored procedure) accesses another object (like a table) in the same database. If both objects have the same owner, SQL Server only checks permissions for the first object, not the second. This is known as ownership chaining.

Cross-database ownership chaining extends this concept across databases. If a stored procedure in Database A accesses a table in Database B, and both objects have the same owner, SQL Server will not check permissions for the table in Database B.

The Risks of Cross-Database Ownership Chaining

While cross-database ownership chaining can simplify permission management, it can also create security risks:

  • Elevation of Privileges: If a user has permission to execute a stored procedure that modifies a table, they can indirectly modify that table even if they don’t have direct permission to do so. This can lead to an unintended elevation of privileges.
  • Database Isolation: Cross-database ownership chaining can undermine database isolation, a key principle in SQL Server security. If a database is compromised, the attacker could potentially access data in other databases.
  • Ownership Confusion: If databases have different owners, it can be difficult to manage and understand the effects of ownership chaining. This can lead to confusion and potential security holes.

Best Practices for Using Cross-Database Ownership Chaining

Given these risks, it’s important to use cross-database ownership chaining sparingly:

  • Limit its use: Only use cross-database ownership chaining when necessary. It’s often better to grant explicit permissions on a per-object basis.
  • Understand ownership: Be clear about who owns what in your databases. This can help prevent unintended consequences from ownership chaining.
  • Monitor and audit: Regularly monitor and audit your SQL Server instances to detect any misuse of cross-database ownership chaining.

In conclusion, while cross-database ownership chaining can be a useful feature in SQL Server, it’s a double-edged sword that must be handled with care. By understanding its risks and following best practices, you can use it effectively while maintaining a strong security posture.

I have provided a few scripts below to assist in your review of this feature in your own environments. The first script is to determine if cross-database ownership chaining is enabled at the instance or database level. There should be a very good and documented reason to have this enabled, and if not, discussions should be had to disable it – with the required testing first. A value of 0 is ideal for most environments. Keep in mind that master, msdb, and tempdb require ‘is_db_chaining_on’ to be on.

SELECT name, value_in_use FROM sys.configurations WHERE name LIKE '%chaining%'
SELECT name, is_db_chaining_on FROM sys.databases WHERE database_id > 4

This next script allows you to see what objects not owned by dbo, sa or the built-in roles, this is a slight modification of the old script by staggerlee011 here. As always, testing any changes is necessary, and as mentioned before in the database ownership post, sa is not necessarily the best option for object ownership.

/*
 
Query to Audit User Owned Objects
 
-- List all Databases owned by a user
-- List all Agent Jobs owned by a user
-- List all Packages owned by a user
-- List all Schemas owned by a user
-- List all Objects owned by a user
-- List all endpoints owned by a user
-- List all Event objects owned by a user
 
*/
IF OBJECT_ID('tempdb..#ownerTable') IS NOT NULL
    DROP TABLE #ownerTable
CREATE TABLE #ownerTable
    (
      [Issue] VARCHAR(100) ,
      [Database] VARCHAR(200) ,
      [Object] VARCHAR(200) ,
      [ObjectType] VARCHAR(200) ,
      [Owner] VARCHAR(200)
    )
 
/*
-- List all Non SA Database Owners
---------------------------------------------------------------------------------------------------------------------- */
INSERT  INTO #ownerTable
        ( [Issue] ,
          [Database] ,
          [Owner]
        )
        SELECT  'Database Owned by a User' ,
                name AS 'Name' ,
                SUSER_SNAME(owner_sid) AS 'Owner'
        FROM    sys.databases
        WHERE   SUSER_SNAME(owner_sid) <> 'sa';
 
/*
-- List all Non SA Job Owners
---------------------------------------------------------------------------------------------------------------------- */
INSERT  INTO #ownerTable
        ( [Issue] ,
          [Database] ,
          [Object] ,
          [Owner]
        )
        SELECT  'Agent Job Owned by a User' ,
                'msdb' ,
                s.name AS 'Job Name' ,
                l.name AS ' Owner'
        FROM    msdb..sysjobs s
                LEFT JOIN master.sys.syslogins l ON s.owner_sid = l.sid
        WHERE   l.name <> 'sa';
 
/*
-- List all Schemas owned by Users
---------------------------------------------------------------------------------------------------------------------- */
DECLARE @DB_NameSch VARCHAR(100);
DECLARE @CommandSch NVARCHAR(MAX);
DECLARE database_cursor CURSOR
FOR
    SELECT  name
    FROM    sys.databases
    WHERE   state_desc = 'ONLINE'
            AND user_access_desc = 'MULTI_USER';
 
OPEN database_cursor;
 
FETCH NEXT FROM database_cursor INTO @DB_NameSch;
 
WHILE @@FETCH_STATUS = 0
    BEGIN
        SELECT  @CommandSch = 'USE [' + @DB_NameSch + '] SELECT ' + ''''
                + 'Schema Owned by a User'  + '''' + ' AS [Issue], 
						' + '''' + @DB_NameSch + ''''
                + ', s.name AS [Object], ''Schema'' AS [ObjectType],  u.name AS [Owner] FROM sys.schemas s INNER JOIN sys.sysusers u ON u.uid = s.principal_id 
				WHERE s.name <> u.name
					AND u.name not in (''dbo'')';
       -- PRINT @CommandSch
        INSERT  INTO #ownerTable
                ( [Issue] ,
                  [Database] ,
                  [Object] ,
				  [ObjectType],
                  [Owner]
                )
                EXEC sp_executesql @CommandSch;
 
        FETCH NEXT FROM database_cursor INTO @DB_NameSch;
    END;
 
CLOSE database_cursor;
DEALLOCATE database_cursor;
 
/*
-- Objects Owned by User
---------------------------------------------------------------------------------------------------------------------- */
 
DECLARE @DB_NameObj VARCHAR(100)
DECLARE @CommandObj NVARCHAR(MAX)
DECLARE database_cursor CURSOR
FOR
    SELECT  name
    FROM    sys.databases
    WHERE state_desc = 'ONLINE' AND user_access_desc = 'MULTI_USER'
 
OPEN database_cursor
 
FETCH NEXT FROM database_cursor INTO @DB_NameObj
 
WHILE @@FETCH_STATUS = 0
    BEGIN
        SELECT  @CommandObj = 'USE [' + @DB_NameObj
                + '] ;
WITH    objects_cte
          AS ( SELECT   o.name ,
                        o.type_desc ,
                        CASE WHEN o.principal_id IS NULL THEN s.principal_id
                             ELSE o.principal_id
                        END AS principal_id
               FROM     sys.objects o
                        INNER JOIN sys.schemas s ON o.schema_id = s.schema_id
               WHERE    o.is_ms_shipped = 0
                        AND o.type IN ( ''U'', ''FN'', ''FS'', ''FT'', ''IF'', ''P'', ''PC'',
                                        ''TA'', ''TF'', ''TR'', ''V'' )
             )
    SELECT  ''Object Owned by User'',
    DB_NAME() AS ''Database'',
    cte.name AS ''Object'',
    cte.type_desc AS ''ObjectType'',
    dp.name AS ''Owner''
    FROM    objects_cte cte
            INNER JOIN sys.database_principals dp ON cte.principal_id = dp.principal_id
    WHERE   dp.name NOT IN ( ''dbo'', ''cdc'');'
        -- PRINT @CommandObj
        -- List all Non SA Package Owners
        INSERT  INTO #ownerTable
                ( [Issue] ,
                  [Database] ,
                  [Object] ,
                  [ObjectType] ,
                  [Owner]
                )
                EXEC sp_executesql @CommandObj
 
        FETCH NEXT FROM database_cursor INTO @DB_NameObj
    END
 
CLOSE database_cursor
DEALLOCATE database_cursor


/*
-- EndPoints Owned by User
---------------------------------------------------------------------------------------------------------------------- */
INSERT  INTO #ownerTable
        ( [Issue] ,
          [Database] ,
          [Object] ,
          [ObjectType] ,
          [Owner]
        )
        SELECT  'Mirroring Endpoint owned by user' ,
                'Server' ,
                name ,
                'EndPoint' ,
                SUSER_NAME(principal_id)
        FROM    sys.endpoints
        WHERE   SUSER_NAME(principal_id) <> 'sa';


/*
-- Show results
---------------------------------------------------------------------------------------------------------------------- */
SELECT  *
FROM    #ownerTable;
 
-- drop table
DROP TABLE #ownerTable;

This last script, modified but original from Martin Smith here,  is to help with auditing the stored procedures which point to another database, each should be reviewed regularly, especially if you are using cross-database ownership chaining in your environment:

IF OBJECT_ID('tempdb..#crossdbprocedures') IS NOT NULL
    DROP TABLE #crossdbprocedures

CREATE TABLE #crossdbprocedures (
    DatabaseName NVARCHAR(256)
    , schema_name NVARCHAR (256)
	, object_name NVARCHAR(256)
	, referenced_database_name NVARCHAR(256)
	, referenced_entity_name NVARCHAR(256)
    )

 INSERT #crossdbprocedures
EXEC sp_msforeachdb '
USE [?];
SELECT	DB_NAME() as databasename
		,	OBJECT_SCHEMA_NAME(referencing_id) as schema_name
		,	OBJECT_NAME(referencing_id) as object_name
		,	referenced_database_name
		,	referenced_entity_name
FROM   sys.sql_expression_dependencies
WHERE  referenced_database_name <> DB_NAME() /*Not NULL and not current DB*/
       AND OBJECTPROPERTY(referencing_id, ''IsProcedure'') = 1 
	   AND DB_NAME() NOT IN (''master'',''model'',''msdb'',''tempdb'')
'
SELECT * FROM #crossdbprocedures
David Seis
Article by David Seis
David Seis is a Support Technician that joined Straight Path in the summer of 2021. His goal is to work in an area that he believes helps others, and to do it to the best of his ability. He loves to help, solve problems, and make things easy for others to understand. Because of this he is lovingly known as a “Good helper” among friends and family. Outside of work, he has a growing passion for cybersecurity, specifically helping protect those who are most vulnerable to fraud, scams, and phishing. He looks forward to learning more about each of his clients, helping minimize issues, and ensure that any problems are solved quickly.

Subscribe for Updates

Name

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Share This