{"id":111846,"date":"2026-08-10T12:00:00","date_gmt":"2026-08-10T12:00:00","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=111846"},"modified":"2026-07-24T11:25:09","modified_gmt":"2026-07-24T11:25:09","slug":"msdb-attack-paths-how-to-secure-sql-server-agent-triggers-and-restores","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/msdb-attack-paths-how-to-secure-sql-server-agent-triggers-and-restores\/","title":{"rendered":"MSDB attack paths: how to secure SQL Server Agent, triggers, and restores"},"content":{"rendered":"\n<p><strong>SQL Server DBAs routinely rely on automation: SQL Server&nbsp;Agent&nbsp;jobs, maintenance plans, refresh processes, restore scripts, replication cleanup, CDC operations, cross-database modules, and temporary objects. These workflows are often trusted because they&#8217;re <em>normal<\/em> &#8211; and that&#8217;s precisely why they deserve more attention.&nbsp;<\/strong><\/p>\n\n\n\n<p><strong>The goal of this article is not to provide&nbsp;exploit&nbsp;recipes. Instead, it&#8217;s to help DBAs answer a practical question: how do I know whether my SQL Server instances are exposed to this kind of risk, what should I&nbsp;monitor, and what should I change when I find a problem?&nbsp;<\/strong><\/p>\n\n\n\n<div id=\"callout-block_3282bc47b4420c4ba1994eb3b1e1a81e\" 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><strong>Working assumption<\/strong>&nbsp;<br>Every section of this article uses the same defensive pattern: identify the trust boundary, look for risky signs, monitor the right events and catalog state, then apply a mitigation that reduces privilege, removes ambiguity, or prevents untrusted code from executing under a privileged context.&nbsp;<\/p>\n\n<\/div>\n<\/div> \n\n\n<p><em>This article is part of <a href=\"https:\/\/www.red-gate.com\/simple-talk\/collections\/fabiano-amorims-complete-guide-to-sql-server-security\/\" target=\"_blank\" rel=\"noreferrer noopener\">Fabiano Amorim&#8217;s complete guide to SQL Server security<\/a> on Simple Talk.<\/em><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-msdb-and-sql-server-agent-the-operational-trust-boundary-nbsp\">MSDB and SQL Server Agent: the operational trust boundary&nbsp;<\/h2>\n\n\n\n<p><strong>DBAs sometimes treat&nbsp;<code>msdb<\/code>&nbsp;as <em>just another system database<\/em>, but it&#8217;s not. In fact, <code>msdb<\/code>&nbsp;is the operational control plane for a SQL Server instance.<\/strong> <\/p>\n\n\n\n<p>It stores <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&nbsp;Agent<\/a>&nbsp;jobs, schedules, job steps, operators, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/database-administration-sql-server\/sql-server-alerts-soup-to-nuts\/\" target=\"_blank\" rel=\"noreferrer noopener\">alerts<\/a>, proxy configuration, credentials, <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/database-mail\/database-mail?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">Database Mail<\/a> metadata, backup and restore history, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/other\/ssis-tricks-would-know\/\" target=\"_blank\" rel=\"noreferrer noopener\">SSIS<\/a>-related information, and <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/database-administration-sql-server\/database-maintenance-plans-in-ssms-an-overview\/\" target=\"_blank\" rel=\"noreferrer noopener\">maintenance plan<\/a> metadata.&nbsp;<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-why-normal-sql-server-agent-permissions-can-create-an-attack-path-nbsp\">Why &#8216;normal&#8217; SQL Server Agent permissions can create an attack path&nbsp;<\/h3>\n\n\n\n<p><strong>A user who can alter the wrong object in&nbsp;<code>msdb<\/code>&nbsp;does <em>not<\/em> necessarily need to be sysadmin to create risk. The dangerous pattern is indirect execution: a lower-privileged principal&nbsp;modifies&nbsp;something that a higher-privileged job later executes. The attacker, or accidental misconfiguration, doesn&#8217;t need to run the payload directly &#8211; the next scheduled job run does it.&nbsp;<\/strong><\/p>\n\n\n\n<p>This is especially important when privileged maintenance jobs are owned by&nbsp;<code><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/why-disabling-the-sql-server-sa-account-still-matters-in-2026\/\" target=\"_blank\" rel=\"noreferrer noopener\">sa<\/a><\/code>, run under the SQL Server Agent service account, or use proxies with access outside the database engine. A job step that looks like routine maintenance may become a server-level execution path if an untrusted principal can&nbsp;modify&nbsp;the job, a <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/40-problems-sql-server-stored-procedure\/\" target=\"_blank\" rel=\"noreferrer noopener\">stored procedure<\/a> the job calls, a&nbsp;table&nbsp;the job reads, or a proxy the job uses.&nbsp;<\/p>\n\n\n\n<div id=\"callout-block_3282bc47b4420c4ba1994eb3b1e1a81e\" 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><strong>DBA lens<\/strong>&nbsp;<br>The question is not only &#8216;who can run this job?&#8217; but also &#8216;who can modify anything this job trusts?&#8217;. That includes the job owner, job steps, schedules, proxies, credentials, called procedures, tables used as queues, and&nbsp;triggers on&nbsp;objects touched by the job.&nbsp;<\/p>\n\n<\/div>\n<\/div> \n\n\n<h3 class=\"wp-block-heading\" id=\"h-what-are-the-signs-a-dba-should-look-for\">What are the signs a DBA should look for?<\/h3>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Application service accounts, developer logins, vendor accounts, or reporting users in&nbsp;<code>SQLAgentUserRole<\/code>,&nbsp;<code>SQLAgentReaderRole<\/code>, or&nbsp;<code>SQLAgentOperatorRole<\/code>.&nbsp;<\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Non-administrative accounts granted&nbsp;<code>db_owner<\/code>,&nbsp;<code>db_ddladmin<\/code>,&nbsp;<code>db_securityadmin<\/code>, or broad explicit <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/theory-and-design\/data-control-language-aka-security\/\" target=\"_blank\" rel=\"noreferrer noopener\">DDL (data definition language)<\/a> permissions in&nbsp;<code>msdb<\/code>.&nbsp;<\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>SQL Server Agent jobs owned by individual users rather than&nbsp;<code>sa<\/code>, a dedicated job-owner login, or an approved administrative group.&nbsp;<\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Job steps that run&nbsp;<a href=\"https:\/\/learn.microsoft.com\/en-us\/ssms\/agent\/create-a-cmdexec-job-step\" target=\"_blank\" rel=\"noreferrer noopener\">CmdExec<\/a>, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/sysadmin\/powershell\/\" target=\"_blank\" rel=\"noreferrer noopener\">PowerShell<\/a>, SSIS, <a href=\"https:\/\/en.wikipedia.org\/wiki\/ActiveX\" target=\"_blank\" rel=\"noreferrer noopener\">ActiveX<\/a>, or&nbsp;operating-system-facing actions without an approved proxy model.&nbsp;<\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Job definitions or schedules&nbsp;modified&nbsp;outside an approved maintenance window.&nbsp;<\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Stored procedures in&nbsp;<code>msdb<\/code>, or in user databases, called by privileged jobs and writable by non-privileged users.&nbsp;<\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Maintenance jobs that read work items from user-editable tables, predictable global <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/temporary-tables-in-sql-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">temporary tables<\/a>, or queues with weak ownership controls.&nbsp;<\/li>\n<\/ul>\n<\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"h-what-to-nbsp-monitor-nbsp-and-how-nbsp\">What to&nbsp;monitor&nbsp;and how&nbsp;<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-sql-agent-role-membership-in-nbsp-msdb-nbsp\">SQL Agent role membership in&nbsp;msdb&nbsp;<\/h4>\n\n\n\n<p>Start with a daily inventory of the fixed SQL Server Agent roles. This should be an allow-list control, not a passive report. Any account outside the expected DBA or automation group should be investigated.&nbsp;<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE msdb; \nGO \nSELECT \n    roles.name  AS agent_role, \n    members.name AS member_name, \n    members.type_desc AS member_type \nFROM sys.database_role_members AS drm \nJOIN sys.database_principals AS roles \n    ON roles.principal_id = drm.role_principal_id \nJOIN sys.database_principals AS members \n    ON members.principal_id = drm.member_principal_id \nWHERE roles.name IN \n( \n    N'SQLAgentUserRole', \n    N'SQLAgentReaderRole', \n    N'SQLAgentOperatorRole' \n) \nORDER BY roles.name, members.name; <\/pre><\/div>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-msdb-nbsp-high-privilege-database-roles-nbsp\">msdb&nbsp;high-privilege database roles&nbsp;<\/h4>\n\n\n\n<p>The Agent roles are not the only concern. A user with&nbsp;<code>db_owner<\/code>&nbsp;or broad DDL rights in&nbsp;<code>msdb<\/code>&nbsp;can often change objects that Agent jobs depend on. Review fixed database role membership and explicit grants.&nbsp;<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE msdb; \nGO \nSELECT \n    roles.name AS role_name, \n    members.name AS member_name, \n    members.type_desc AS member_type \nFROM sys.database_role_members AS drm \nJOIN sys.database_principals AS roles \n    ON roles.principal_id = drm.role_principal_id \nJOIN sys.database_principals AS members \n    ON members.principal_id = drm.member_principal_id \nWHERE roles.name IN \n( \n    N'db_owner', \n    N'db_ddladmin', \n    N'db_securityadmin', \n    N'db_accessadmin' \n) \nORDER BY roles.name, members.name; <\/pre><\/div>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE msdb; \nGO \nSELECT \n    USER_NAME(dp.grantee_principal_id) AS grantee_name, \n    dp.state_desc, \n    dp.permission_name, \n    dp.class_desc, \n    OBJECT_SCHEMA_NAME(dp.major_id) AS object_schema, \n    OBJECT_NAME(dp.major_id) AS object_name \nFROM sys.database_permissions AS dp \nWHERE dp.grantee_principal_id NOT IN (0, 1, 2) \n  AND dp.permission_name IN \n  ( \n      N'ALTER', N'CONTROL', N'EXECUTE', N'IMPERSONATE', \n      N'TAKE OWNERSHIP', N'VIEW DEFINITION' \n  ) \nORDER BY grantee_name, permission_name; <\/pre><\/div>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-sql-agent-job-ownership-and-dangerous-subsystems-nbsp\">SQL Agent job ownership and dangerous subsystems&nbsp;<\/h4>\n\n\n\n<p>Job ownership is&nbsp;security-relevant. A job owned by a sysadmin runs with elevated behavior in many paths, while a job owned by a normal user may fail or behave differently. The risky case is not only the owner itself; it is a privileged owner combined with weaker modification control.<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE msdb; \nGO \nSELECT \n    j.name AS job_name, \n    SUSER_SNAME(j.owner_sid) AS job_owner, \n    j.enabled, \n    j.date_created, \n    j.date_modified \nFROM dbo.sysjobs AS j \nWHERE SUSER_SNAME(j.owner_sid) NOT IN \n( \n    N'sa', \n    N'DOMAIN\\ApprovedSqlAgentOwners' \n) \nORDER BY j.date_modified DESC; <\/pre><\/div>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE msdb; \nGO \nSELECT \n    j.name AS job_name, \n    SUSER_SNAME(j.owner_sid) AS job_owner, \n    s.step_id, \n    s.step_name, \n    s.subsystem, \n    s.proxy_id, \n    p.name AS proxy_name, \n    s.database_name, \n    LEFT(s.command, 4000) AS command_sample \nFROM dbo.sysjobsteps AS s \nJOIN dbo.sysjobs AS j \n    ON j.job_id = s.job_id \nLEFT JOIN dbo.sysproxies AS p \n    ON p.proxy_id = s.proxy_id \nWHERE s.subsystem IN (N'CmdExec', N'PowerShell', N'SSIS', N'ActiveScripting') \n   OR s.proxy_id IS NOT NULL \nORDER BY j.name, s.step_id; <\/pre><\/div>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-job-changes-and-failed-execution-probes-nbsp\">Job changes and failed execution probes&nbsp;<\/h4>\n\n\n\n<p>SQL Server Agent history is not a complete security log, but is still useful for spotting abnormal behavior. Look for recent modifications, job failures after&nbsp;permission&nbsp;errors, and unexpected subsystem usage.&nbsp;<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">USE msdb; \nGO \nSELECT TOP (200) \n    j.name AS job_name, \n    h.step_id, \n    h.step_name, \n    h.run_date, \n    h.run_time, \n    h.run_status, \n    h.sql_message_id, \n    h.sql_severity, \n    LEFT(h.message, 2000) AS message \nFROM dbo.sysjobhistory AS h \nJOIN dbo.sysjobs AS j \n    ON j.job_id = h.job_id \nWHERE h.run_status &lt;&gt; 1 \nORDER BY h.instance_id DESC; <\/pre><\/div>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-sql-audit-events-for-nbsp-msdb-nbsp\">SQL Audit events for&nbsp;<code>msdb<\/code>&nbsp;<\/h4>\n\n\n\n<p>Use SQL Server Audit for durable records of permission and object changes. At minimum, consider auditing role membership changes, database permission changes, schema\/object changes in&nbsp;<code>msdb<\/code>, backup\/restore operations, and server-level permission changes. Tune audit targets and filters for your&nbsp;environment&nbsp;so the output is reviewable.&nbsp;<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">\/* Example pattern - adapt file paths, retention, and filters to your standard. *\/ \nUSE master; \nGO \nCREATE SERVER AUDIT [Audit_SQLServer_Security] \nTO FILE (FILEPATH = N'D:\\SQLAudit\\', MAXSIZE = 1024 MB, MAX_ROLLOVER_FILES = 20) \nWITH (QUEUE_DELAY = 1000, ON_FAILURE = CONTINUE); \nGO \nALTER SERVER AUDIT [Audit_SQLServer_Security] WITH (STATE = ON); \nGO \n \nCREATE SERVER AUDIT SPECIFICATION [ServerAudit_SecurityBoundaryChanges] \nFOR SERVER AUDIT [Audit_SQLServer_Security] \n    ADD (SERVER_ROLE_MEMBER_CHANGE_GROUP), \n    ADD (SERVER_PERMISSION_CHANGE_GROUP), \n    ADD (DATABASE_CHANGE_GROUP), \n    ADD (BACKUP_RESTORE_GROUP) \nWITH (STATE = ON); \nGO \n \nUSE msdb; \nGO \nCREATE DATABASE AUDIT SPECIFICATION [DBAudit_msdb_Changes] \nFOR SERVER AUDIT [Audit_SQLServer_Security] \n    ADD (DATABASE_ROLE_MEMBER_CHANGE_GROUP), \n    ADD (DATABASE_PERMISSION_CHANGE_GROUP), \n    ADD (SCHEMA_OBJECT_CHANGE_GROUP), \n    ADD (DATABASE_OBJECT_CHANGE_GROUP), \n    ADD (DATABASE_OBJECT_PERMISSION_CHANGE_GROUP) \nWITH (STATE = ON); \nGO <\/pre><\/div>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-recommended-fixes-and-mitigations-nbsp\">Recommended fixes and mitigations&nbsp;<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-restrict-agent-role-membership\">Restrict Agent role membership<\/h4>\n\n\n\n<p id=\"h-restrict-agent-role-membershiponly-trusted-dbas-and-operational-service-accounts-should-be-members-of-sqlagentuserrole-sqlagentreaderrole-or-sqlagentoperatorrole-treat-these-roles-as-administrative\">Only trusted DBAs and operational service accounts should be members of&nbsp;<code>SQLAgentUserRole<\/code>,&nbsp;<code>SQLAgentReaderRole<\/code>, or&nbsp;<code>SQLAgentOperatorRole<\/code>. Treat these roles as administrative.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-separate-job-ownership-from-job-editing\">Separate job ownership from job editing<\/h4>\n\n\n\n<p id=\"h-separate-job-ownership-from-job-editingprivileged-jobs-should-have-standardized-owners-but-lower-privileged-users-should-not-be-able-to-modify-those-jobs-the-procedures-they-call-or-tables-they-trust\">Privileged jobs <em>should<\/em> have standardized owners, but lower-privileged users shouldn&#8217;t be able to&nbsp;modify&nbsp;them, the procedures they call, or tables they trust.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-control-proxies-and-credentials\">Control proxies and credentials<\/h4>\n\n\n\n<p id=\"h-control-proxies-and-credentialsproxy-accounts-should-have-minimum-os-and-sql-privileges-avoid-shared-high-privilege-proxies-for-convenience\">Proxy accounts should have&nbsp;minimum&nbsp;OS and SQL privileges. Avoid shared high-privilege proxies for convenience.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-baseline-job-definitions\">Baseline job definitions<\/h4>\n\n\n\n<p>Export job definitions to source control or a signed configuration baseline. Alert when job steps, schedules, owners, proxies, or command text change.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-avoid-user-writable-command-queues\">Avoid user-writable command queues<\/h4>\n\n\n\n<p>If jobs read work from tables,&nbsp;validate&nbsp;ownership, <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>, <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>, and input sanitization. Don&#8217;t let low-privileged users write directly into privileged job control tables.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-protect-msdb\">Protect msdb<\/h4>\n\n\n\n<p id=\"h-protect-msdbnever-grant-db-owner-in-msdb-to-application-teams-or-vendors-only-grant-the-minimum-agent-capability-required-and-review-it-regularly\">Never grant&nbsp;<code>db_owner<\/code>&nbsp;in&nbsp;<code>msdb<\/code>&nbsp;to application teams or vendors. Only grant the&nbsp;minimum&nbsp;Agent capability&nbsp;required, and review it regularly.&nbsp;<\/p>\n\n\n\n<section id=\"my-first-block-block_90655288c8720a51f922231a05478a43\" 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-trigger-aware-maintenance-when-trusted-jobs-run-untrusted-code-nbsp-and-why-privileged-maintenance-jobs-can-be-hijacked-through-triggers\">Trigger-aware maintenance: when trusted jobs run untrusted code&nbsp;(and why privileged maintenance jobs can be hijacked through triggers)<\/h2>\n\n\n\n<p><strong>Triggers are a classic example of code that runs because something else happened. It&#8217;s useful for auditing and enforcement but creates a permission-hijacking risk.<\/strong> <\/p>\n\n\n\n<p>A <a href=\"https:\/\/www.sqlservercentral.com\/articles\/learning-about-dml-triggers\" target=\"_blank\" rel=\"noreferrer noopener\">DML&nbsp;trigger<\/a>&nbsp;fires when a table is&nbsp;modified. A&nbsp;<a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/triggers\/ddl-triggers?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">DDL&nbsp;trigger<\/a>&nbsp;fires when a matching DDL event occurs. In many cases, the trigger&nbsp;executes in&nbsp;the security context of the statement that fired it, rather than the user who originally created the trigger.&nbsp;<\/p>\n\n\n\n<p>This means a lower-privileged database owner, developer, or compromised application account may be able to create a trigger that waits for a privileged DBA or Agent job to touch the database. When the privileged maintenance process runs <code>ALTER INDEX<\/code>, <code>UPDATE STATISTICS<\/code>, certain schema changes, or data cleanup, the trigger fires in that privileged context.&nbsp;<\/p>\n\n\n\n<p>Backup and <code>DBCC CHECKDB<\/code> aren&#8217;t the trigger-hijacking examples most DBAs should worry about. The more relevant operations are the ones that issue DDL or DML inside user databases, especially index maintenance, statistics maintenance, ETL cleanup, replication cleanup, CDC enable\/disable operations, and ad-hoc remediation scripts.&nbsp;<\/p>\n\n\n\n<div id=\"callout-block_3282bc47b4420c4ba1994eb3b1e1a81e\" 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><strong>An important distinction&nbsp;<\/strong><br>The problem is <em>not<\/em> that triggers are always unsafe. Rather, the issues begin when privileged code touches objects or databases that lower-privileged users can influence. The maintenance process then becomes a bridge between two trust levels.&nbsp;<\/p>\n\n<\/div>\n<\/div> \n\n\n<h2 class=\"wp-block-heading\" id=\"h-signs-a-dba-should-look-for-nbsp\">Signs a DBA should look for&nbsp;<\/h2>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>DDL&nbsp;triggers in&nbsp;user databases where local&nbsp;<code>db_owner<\/code>&nbsp;users are not trusted as server administrators.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Server-level DDL triggers&nbsp;not&nbsp;being documented in the change-management process.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/en.wikipedia.org\/wiki\/Data_manipulation_language\" target=\"_blank\" rel=\"noreferrer noopener\">DML (data manipulation language)<\/a>&nbsp;triggers on&nbsp;tables touched by privileged SQL Agent jobs, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/data-security-privacy-compliance\/sql-server-privilege-escalation-via-replication-jobs\/\" target=\"_blank\" rel=\"noreferrer noopener\">replication jobs<\/a>, <a href=\"https:\/\/www.qlik.com\/us\/change-data-capture\/cdc-change-data-capture\" target=\"_blank\" rel=\"noreferrer noopener\">CDC (change data capture)<\/a> processes, <a href=\"https:\/\/www.ibm.com\/think\/topics\/etl\" target=\"_blank\" rel=\"noreferrer noopener\">ETL (extract, transform, load)<\/a> cleanup, or maintenance scripts.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Triggers created or&nbsp;modified&nbsp;shortly before scheduled maintenance windows.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Triggers that&nbsp;contain&nbsp;dynamic SQL, server-level statements, <code>GRANT<\/code>\/<code>ALTER SERVER ROLE<\/code>, linked server calls,&nbsp;xp_cmdshell, <a href=\"https:\/\/en.wikipedia.org\/wiki\/OLE_Automation\" target=\"_blank\" rel=\"noreferrer noopener\">OLE Automation<\/a>, SQL Agent procedure calls, or cross-database access.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Maintenance plans that can&#8217;t run under a lower-privileged execution context, or can&#8217;t wrap operations in <code>EXECUTE AS USER<\/code>.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Don&#8217;t run broad maintenance jobs as sysadmin when they touch databases controlled by local owners, <em>unless<\/em> the workflow has been reviewed and sandboxed.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>For maintenance inside untrusted databases, use an execution pattern that prevents server-level permission hijacking, such as executing the database-local work under a contained database context (exec as user&nbsp;dbo) where&nbsp;appropriate.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Review maintenance tools and parameters. For example, maintenance frameworks that support an <code><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/statements\/execute-as-transact-sql?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">EXECUTE AS<\/a><\/code> option can help reduce the risk of a sysadmin token being hijacked by a trigger.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Remove unnecessary&nbsp;<code>db_owner<\/code>&nbsp;and broad <code>ALTER<\/code> permissions from application and vendor accounts. Use custom roles with explicit object-level or schema-level grants.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Before privileged migrations or manual DDL against an untrusted database, carry out an inventory on triggers &#8211; and consider disabling non-essential triggers only through a controlled change process.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Document which jobs are expected to touch which databases. Alert when a job starts touching a new database, schema, or object class.&nbsp;<\/li>\n<\/ul>\n<\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"h-what-to-nbsp-monitor-nbsp-and-how-nbsp-0\">What to&nbsp;monitor&nbsp;and how&nbsp;<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-inventory-database-and-server-triggers-nbsp\">Inventory database and server triggers&nbsp;<\/h4>\n\n\n\n<p>Inventory is the first control. You can&#8217;t protect a privileged maintenance job from untrusted triggers if you don&#8217;t know which triggers actually exist.<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">\/* Run in each user database. *\/ \nSELECT \n    DB_NAME() AS database_name, \n    t.name AS trigger_name, \n    t.parent_class_desc, \n    OBJECT_SCHEMA_NAME(t.parent_id) AS parent_schema, \n    OBJECT_NAME(t.parent_id) AS parent_object, \n    t.is_disabled, \n    t.create_date, \n    t.modify_date, \n    OBJECT_DEFINITION(t.object_id) AS trigger_definition \nFROM sys.triggers AS t \nORDER BY t.modify_date DESC; <\/pre><\/div>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">\/* Server-level DDL triggers. *\/ \nSELECT \n    name AS trigger_name, \n    parent_class_desc, \n    is_disabled, \n    create_date, \n    modify_date, \n    OBJECT_DEFINITION(object_id) AS trigger_definition \nFROM sys.server_triggers \nORDER BY modify_date DESC; <\/pre><\/div>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-look-for-suspicious-trigger-content\">Look for suspicious trigger content<\/h4>\n\n\n\n<p>Run the following in each user database. Adjust patterns to your standards.<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT \n    DB_NAME() AS database_name, \n    t.name AS trigger_name, \n    t.parent_class_desc, \n    t.is_disabled, \n    t.modify_date, \n    m.definition \nFROM sys.triggers AS t \nJOIN sys.sql_modules AS m \n    ON m.object_id = t.object_id \nWHERE m.definition LIKE N'%ALTER SERVER ROLE%' \n   OR m.definition LIKE N'%CONTROL SERVER%' \n   OR m.definition LIKE N'%sp_addsrvrolemember%' \n   OR m.definition LIKE N'%xp_cmdshell%' \n   OR m.definition LIKE N'%sp_start_job%' \n   OR m.definition LIKE N'%EXEC%(%' \n   OR m.definition LIKE N'%sp_executesql%' \n   OR m.definition LIKE N'%AT [%'       -- linked server execution pattern \n   OR m.definition LIKE N'%OPENQUERY%' \nORDER BY t.modify_date DESC; <\/pre><\/div>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-audit-trigger-creation-and-modification-nbsp\">Audit trigger creation and modification&nbsp;<\/h4>\n\n\n\n<p>For <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 Audit<\/a>, database-level object and schema change groups can capture many trigger-related DDL events. In environments with high volume, scope the audit to high-risk databases or filter downstream in the <a href=\"https:\/\/www.splunk.com\/en_us\/blog\/learn\/siem-security-information-event-management.html\" target=\"_blank\" rel=\"noreferrer noopener\">SIEM<\/a>.&nbsp;<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">\/* Database audit specification pattern for high-risk user databases. *\/ \nCREATE DATABASE AUDIT SPECIFICATION [DBAudit_DDL_And_Trigger_Changes] \nFOR SERVER AUDIT [Audit_SQLServer_Security] \n    ADD (SCHEMA_OBJECT_CHANGE_GROUP), \n    ADD (DATABASE_OBJECT_CHANGE_GROUP), \n    ADD (DATABASE_PERMISSION_CHANGE_GROUP) \nWITH (STATE = ON); \nGO <\/pre><\/div>\n\n\n\n<p><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/database-administration-sql-server\/extended-events-data-collection\/\" target=\"_blank\" rel=\"noreferrer noopener\">Extended Events<\/a> can also be used to observe DDL and object-alteration activity. <\/p>\n\n\n\n<p>The exact event set should be tested on your SQL Server version and workload, but a practical starting point is capturing&nbsp;<code>object_created<\/code>,&nbsp;<code>object_altered<\/code>,&nbsp;<code>object_deleted<\/code>, and&nbsp;<code>ddl_database_level_events<\/code>, with&nbsp;<code>sql_text<\/code>,&nbsp;<code>database_name<\/code>, <code>username<\/code>, and&nbsp;<code>client_hostname<\/code>&nbsp;actions.&nbsp;<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">CREATE EVENT SESSION [XE_DDL_Trigger_Object_Changes] \n\nON SERVER \n\nADD EVENT sqlserver.object_created \n\n( \n\n    ACTION(sqlserver.client_hostname, sqlserver.database_name, \n\n           sqlserver.server_principal_name, sqlserver.sql_text) \n\n), \n\nADD EVENT sqlserver.object_altered \n\n( \n\n    ACTION(sqlserver.client_hostname, sqlserver.database_name, \n\n           sqlserver.server_principal_name, sqlserver.sql_text) \n\n), \n\nADD EVENT sqlserver.object_deleted \n\n( \n\n    ACTION(sqlserver.client_hostname, sqlserver.database_name, \n\n           sqlserver.server_principal_name, sqlserver.sql_text) \n\n) \n\nADD TARGET package0.event_file \n\n( \n\n    SET filename = N'D:\\XE\\XE_DDL_Trigger_Object_Changes.xel', \n\n        max_file_size = 100, \n\n        max_rollover_files = 10 \n\n) \n\nWITH (STARTUP_STATE = ON); \n\nGO \n\nALTER EVENT SESSION [XE_DDL_Trigger_Object_Changes] ON SERVER STATE = START; \n\nGO<\/pre><\/div>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-correlate-trigger-changes-with-maintenance-windows-nbsp\">Correlate trigger changes with maintenance windows&nbsp;<\/h4>\n\n\n\n<p>The most useful detection is often <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/mdx\/correlation-mdx?view=sql-server-ver17#:~:text=The%20Correlation%20function%20calculates%20the,values%20for%20the%20y%2Daxis.\" target=\"_blank\" rel=\"noreferrer noopener\">correlation<\/a>, not a single event. A trigger created at 01:55 followed by an index maintenance job at 02:00, for example, is more suspicious than either event alone. Store job schedules, job runs, trigger create\/modify&nbsp;times, and DDL audit records in&nbsp;a central place, and&nbsp;alert on&nbsp;proximity.&nbsp;<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">\/* Example: recent triggers modified in a maintenance-sensitive window. *\/ \nSELECT \n    DB_NAME() AS database_name, \n    t.name AS trigger_name, \n    t.parent_class_desc, \n    t.create_date, \n    t.modify_date, \n    DATEDIFF(MINUTE, t.modify_date, SYSDATETIME()) AS minutes_since_modify \nFROM sys.triggers AS t \nWHERE t.modify_date &gt;= DATEADD(HOUR, -24, SYSDATETIME()) \nORDER BY t.modify_date DESC; <\/pre><\/div>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-recommended-fixes-and-mitigations-nbsp-0\">Recommended fixes and mitigations&nbsp;<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-restrict-trigger-creation\">Restrict trigger creation<\/h4>\n\n\n\n<p>Do <em>not<\/em> grant&nbsp;<code>db_owner<\/code>&nbsp;or broad <code>ALTER<\/code> permissions to users who shouldn&#8217;t be able to influence privileged maintenance.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-inventory-before-privileged-maintenance\">Inventory before privileged maintenance<\/h4>\n\n\n\n<p>Before&nbsp;running sensitive DDL\/DML in an untrusted database, inventory triggers and&nbsp;disable&nbsp;unverified triggers where the operational risk allows it.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-use-a-sandboxed-execution-context\">Use a sandboxed execution context<\/h4>\n\n\n\n<p>For maintenance inside databases with untrusted owners or developers, consider running DDL\/DML under <code>EXECUTE AS USER = 'dbo'<\/code>, in order to prevent server-level token hijacking. Validate this pattern in your environment, especially if <code>TRUSTWORTHY<\/code> is <code>ON<\/code>.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-avoid-ssms-maintenance-plans-for-nbsp-sensitive-untrusted-nbsp-databases\">Avoid SSMS maintenance plans for&nbsp;sensitive untrusted&nbsp;databases<\/h4>\n\n\n\n<p>If you need explicit execution-context control, use scripts or maintenance frameworks that support an execution user parameter.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-treat-nbsp-trigger-changes-nbsp-as-security-events\">Treat&nbsp;trigger changes&nbsp;as security events<\/h4>\n\n\n\n<p>A new or modified trigger in a production database should be visible to DBAs, security operations, or both.&nbsp;<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-untrusted-restores-why-a-backup-file-is-not-just-data-and-why-restore-workflows-are-a-security-boundary\">Untrusted restores: why a backup file is not just data, and why restore workflows are a security boundary<\/h2>\n\n\n\n<p>A SQL Server backup carries database metadata, owners, SIDs, modules, triggers, assemblies, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/learn\/service-broker-advanced-basics-workbench\/\" target=\"_blank\" rel=\"noreferrer noopener\">Service Broker<\/a> configuration, replication artifacts, CDC objects, permissions, schemas, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/mastering-sql-views\/\" target=\"_blank\" rel=\"noreferrer noopener\">views<\/a>, indexed views, and many other pieces of executable or security-relevant state.&nbsp;<\/p>\n\n\n\n<p>Microsoft documentation is very explicit when it states that <strong>restoring a backup from an untrusted source is a security risk<\/strong>. It goes on to say that <strong>a malicious backup can compromise the SQL Server environment, and can introduce arbitrary code execution before validation scripts have a chance to run<\/strong>. <\/p>\n\n\n\n<p>For DBAs, the takeaway is simple: <strong>an external .bak&nbsp;file should be treated more like untrusted software&nbsp;than like&nbsp;a CSV file.&nbsp;<\/strong><\/p>\n\n\n\n<p>This matters because&nbsp;restores&nbsp;are routine. <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/sql-server-high-availability-and-disaster-recovery-plan\/\" target=\"_blank\" rel=\"noreferrer noopener\">DR (disaster recovery)<\/a> tests, production-to-staging refreshes, vendor troubleshooting, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/cloud\/why-even-the-best-teams-struggle-with-cloud-migrations\/\" target=\"_blank\" rel=\"noreferrer noopener\">migrations<\/a>, client data imports, and QA (quality assurance) refreshes all create pressure to restore quickly. And it&#8217;s here &#8211; the drive for convenience &#8211; where trust-boundary mistakes can and often do happen.&nbsp;<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-practical-attack-paths-to-think-about-defensively-nbsp\">Practical attack paths to think about defensively&nbsp;<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-ownership-smuggling\">Ownership smuggling<\/h4>\n\n\n\n<p id=\"h-ownership-smugglingif-the-owner-sid-embedded-in-the-backup-maps-to-an-existing-privileged-login-on-the-target-instance-the-restored-database-may-arrive-owned-by-that-principal-if-the-sid-does-not-map-ownership-may-fall-to-the-login-performing-the-restore-both-cases-are-security-relevant\">If the&nbsp;owner&nbsp;SID (security identifier) embedded in the backup maps to an existing privileged login on the target instance, the restored database may arrive owned by that principal. If the SID doesn&#8217;t map, ownership may fall to the login performing the restore. Both cases are&nbsp;security-relevant.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-persisted-module-and-trigger-state\">Persisted module and trigger state<\/h4>\n\n\n\n<p>Stored procedures, triggers, functions, and views can arrive with code that later runs under a more privileged workflow.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-indexed-view-and-metadata-trust\">Indexed-view and metadata trust<\/h4>\n\n\n\n<p>SQL Server&nbsp;validates&nbsp;indexed-view rules at creation time. A restored database brings persisted metadata from another environment, so DBAs shouldn&#8217;t assume all restored metadata was produced under the target&nbsp;instance&#8217;s&nbsp;trust model.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-replication-cdc-service-broker-and-clr-artifacts\">Replication, CDC, Service Broker, and CLR artifacts<\/h4>\n\n\n\n<p>These features can carry operational&nbsp;state&nbsp;that&nbsp;interacts&nbsp;with privileged jobs, activation, cleanup, or external execution surfaces.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-post-restore-automation\">Post-restore automation<\/h4>\n\n\n\n<p>The first risky moment is the restore itself &#8211; and then the automation that&nbsp;immediately&nbsp;follows. This includes compatibility fixes, user mapping scripts, index maintenance, statistics updates, ETL validation, and smoke tests.&nbsp;<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-signs-a-dba-should-look-for-nbsp-0\">Signs a DBA should look for&nbsp;<\/h3>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Backups&nbsp;restored&nbsp;from vendors, customers, developers, unknown file shares, email attachments, ticket uploads, or cloud storage locations outside your trusted backup chain.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Restored databases owned by&nbsp;<code>sa<\/code>, a deployment account, a DBA&#8217;s personal login, a sysadmin login, or an unexpected application login.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><code>TRUSTWORTHY ON<\/code>, <code>DB_CHAINING ON<\/code>, enabled Service Broker queues, activation procedures, CLR assemblies, or external access assemblies in newly restored databases.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Unexpected replication, CDC, change tracking, DDL triggers, DML triggers, database-level permissions, orphaned users, or high-privilege database roles after&nbsp;restore.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Post-restore jobs that&nbsp;immediately&nbsp;run maintenance or validation code before security inspection is complete.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Restore operations performed by highly privileged personal accounts rather than controlled automation accounts.&nbsp;<\/li>\n<\/ul>\n<\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"h-what-to-nbsp-monitor-nbsp-and-how-nbsp-1\">What to&nbsp;monitor&nbsp;and how&nbsp;<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-audit-backup-and-restore-commands-nbsp\">Audit backup and restore commands&nbsp;<\/h4>\n\n\n\n<p>Enable SQL Server Audit for <code>BACKUP_RESTORE_GROUP<\/code> at server scope. This gives you a durable log of backup and restore commands. Pair it with Windows, storage, and change-management logs so you can trace where the backup file came from.&nbsp;<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">CREATE SERVER AUDIT SPECIFICATION [ServerAudit_BackupRestore] \nFOR SERVER AUDIT [Audit_SQLServer_Security] \n    ADD (BACKUP_RESTORE_GROUP) \nWITH (STATE = ON); \nGO <\/pre><\/div>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-post-restore-security-gate-nbsp\">Post-restore security gate&nbsp;<\/h4>\n\n\n\n<p>Every&nbsp;restore&nbsp;into a shared or production-adjacent environment should pass through a security gate before application users or maintenance jobs touch it. The following checks are examples of the baseline state to capture&nbsp;immediately&nbsp;after&nbsp;restore.<\/p>\n\n\n\n<p>Instance-level view of newly restored database properties:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT \n    d.name AS database_name, \n    SUSER_SNAME(d.owner_sid) AS owner_name, \n    d.owner_sid, \n    d.is_trustworthy_on, \n    d.is_db_chaining_on, \n    d.is_broker_enabled, \n    d.is_cdc_enabled, \n    d.create_date, \n    d.compatibility_level \nFROM sys.databases AS d \nWHERE d.database_id &gt; 4 \nORDER BY d.create_date DESC; <\/pre><\/div>\n\n\n\n<p>What to run inside the restored database:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT \n    USER_NAME(drm.role_principal_id) AS role_name, \n    USER_NAME(drm.member_principal_id) AS member_name \nFROM sys.database_role_members AS drm \nWHERE USER_NAME(drm.role_principal_id) IN \n( \n    N'db_owner', N'db_securityadmin', N'db_ddladmin', \n    N'db_accessadmin', N'db_backupoperator' \n) \nORDER BY role_name, member_name; <\/pre><\/div>\n\n\n\n<p>Modules that&nbsp;execute&nbsp;under a specific context:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT \n    SCHEMA_NAME(o.schema_id) AS schema_name, \n    o.name AS object_name, \n    o.type_desc, \n    m.execute_as_principal_id, \n    USER_NAME(m.execute_as_principal_id) AS execute_as_user \nFROM sys.sql_modules AS m \nJOIN sys.objects AS o \n    ON o.object_id = m.object_id \nWHERE m.execute_as_principal_id IS NOT NULL \nORDER BY schema_name, object_name;<\/pre><\/div>\n\n\n\n<p>Triggers and enabled status:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT \n    DB_NAME() AS database_name, \n    t.name AS trigger_name, \n    t.parent_class_desc, \n    OBJECT_SCHEMA_NAME(t.parent_id) AS parent_schema, \n    OBJECT_NAME(t.parent_id) AS parent_object, \n    t.is_disabled, \n    t.create_date, \n    t.modify_date \nFROM sys.triggers AS t \nORDER BY t.modify_date DESC;<\/pre><\/div>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-feature-and-artifact-review\">Feature and artifact review<\/h4>\n\n\n\n<p>CDC status:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT DB_NAME() AS database_name, is_cdc_enabled \nFROM sys.databases \nWHERE name = DB_NAME();<\/pre><\/div>\n\n\n\n<p>Change tracking:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT DB_NAME() AS database_name, is_auto_cleanup_on, retention_period, retention_period_units_desc \nFROM sys.change_tracking_databases \nWHERE database_id = DB_ID();<\/pre><\/div>\n\n\n\n<p>Service Broker queues with activation:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT \n    SCHEMA_NAME(q.schema_id) AS schema_name, \n    q.name AS queue_name, \n    q.is_activation_enabled, \n    q.activation_procedure, \n    q.execute_as_principal_id \nFROM sys.service_queues AS q \nWHERE q.is_activation_enabled = 1;<\/pre><\/div>\n\n\n\n<p>Assemblies:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT name, permission_set_desc, is_user_defined \nFROM sys.assemblies \nWHERE is_user_defined = 1;<\/pre><\/div>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-recommended-fixes-and-mitigations-nbsp-1\">Recommended fixes and mitigations&nbsp;<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-create-a-restore-quarantine-tier\">Create a restore quarantine tier<\/h4>\n\n\n\n<p>External or untrusted backups should land first on isolated SQL Server instances with no production trust, no linked servers to sensitive environments, no domain privileges beyond what is&nbsp;required, and no automatic maintenance jobs.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-use-a-controlled-restore-account\">Use a controlled restore account<\/h4>\n\n\n\n<p>Do <em>not<\/em> restore untrusted backups with personal sysadmin accounts if automation can use a controlled account and record provenance.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-normalize-ownership-nbsp-immediately\">Normalize ownership&nbsp;immediately<\/h4>\n\n\n\n<p>After&nbsp;restore, change the database owner to a dedicated low-use administrative owner approved by your policy. Avoid leaving it owned by a personal login or unexpected mapped SID.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-disable-risky-database-properties\">Disable risky database properties<\/h4>\n\n\n\n<p>Set <code>TRUSTWORTHY OFF<\/code> and <code>DB_CHAINING OFF<\/code> unless a documented exception exists.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-inspect-before-touching\">Inspect <em>before<\/em> touching<\/h4>\n\n\n\n<p>Never run index maintenance, update statistics, ETL validation, smoke tests, or application traffic until triggers, modules, Service Broker activation, <a href=\"https:\/\/learn.microsoft.com\/en-us\/dotnet\/standard\/clr\" target=\"_blank\" rel=\"noreferrer noopener\">CLR (Common Language Runtime)<\/a>, replication, CDC, permissions, and owners have been reviewed.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-preserve-evidence\">Preserve evidence<\/h4>\n\n\n\n<p>Record source path, <a href=\"https:\/\/www.techtarget.com\/searchsecurity\/definition\/checksum\" target=\"_blank\" rel=\"noreferrer noopener\">checksum<\/a>, restore operator, restore time, database owner, <code>DB_CHAINING<\/code>, and a pre-sanitization inventory before making changes.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-prefer-data-only-imports-when-possible\">Prefer data-only imports when possible<\/h4>\n\n\n\n<p>For <em>truly<\/em> untrusted data, importing data into pre-created schemas is often safer than restoring an entire database with executable metadata.&nbsp;<\/p>\n\n\n\n<section id=\"my-first-block-block_cc1f261d32892a6b06bacb1f17f2fcbe\" 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-cross-database-ownership-chaining-and-shared-owners-why-shared-ownership-can-bypass-the-permission-model-you-thought-you-had-nbsp\">Cross-database ownership chaining and shared owners: why shared ownership can bypass the permission model you <em>thought<\/em> you had&nbsp;<\/h2>\n\n\n\n<p><strong><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/cross-database-ownership-chaining-in-sql-server-security-risks-behavior-and-privilege-escalation-explained\/\" target=\"_blank\" rel=\"noreferrer noopener\">Cross-database ownership chaining<\/a> is a compatibility feature that can bypass permission checks across databases when ownership aligns.<\/strong> <\/p>\n\n\n\n<p>It exists because some applications were built around modules in one database reading objects in another without explicit grants. The security cost is that the permission boundary between databases becomes much less clear.&nbsp;<\/p>\n\n\n\n<p><em>You can read a lot more about cross-database ownership chaining in my dedicated deep-dive guide <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/cross-database-ownership-chaining-in-sql-server-security-risks-behavior-and-privilege-escalation-explained\/\" target=\"_blank\" rel=\"noreferrer noopener\">here<\/a>.<\/em><\/p>\n\n\n\n<p>The common risky pattern is simple: many databases are owned by&nbsp;<code>sa<\/code>, the same deployment login, or the same application owner. Further, <code>DB_CHAINING<\/code> is enabled, and modules reference objects across database boundaries. <\/p>\n\n\n\n<p>As such, DBAs assume each database is isolated because users were only granted access to one database &#8211; but ownership chaining can make that assumption false.&nbsp;<\/p>\n\n\n\n<p>This becomes even more difficult when&nbsp;<a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/learn\/mastering-tempdb-the-basics\/\" target=\"_blank\" rel=\"noreferrer noopener\"><code>tempdb<\/code><\/a>&nbsp;or global temporary objects are used as workflow glue, as&nbsp;<code>tempdb<\/code>&nbsp;is shared and global temporary tables are visible across sessions. If privileged jobs use predictable global temporary table names, or trust data placed in shared temporary structures, a low-privileged session may be able to race, poison, or&nbsp;observe&nbsp;parts of the workflow.&nbsp;<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-signs-a-dba-should-look-for-nbsp-1\">Signs a DBA should look for&nbsp;<\/h3>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>The server-level cross&nbsp;db&nbsp;ownership chaining option is enabled.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>User databases have&nbsp;<code>is_db_chaining_on&nbsp;= 1<\/code> without a documented exception.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Many unrelated databases share the same owner, especially&nbsp;<code>sa<\/code>&nbsp;or a broad deployment login.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><code>TRUSTWORTHY ON<\/code> appears in user databases, especially when those databases are owned by sysadmin principals.<br>&nbsp;<\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Stored procedures, views, functions, triggers, or jobs&nbsp;reference&nbsp;three-part names across databases.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Agent jobs or application&nbsp;code&nbsp;use predictable global temporary tables such as ##WorkQueue, ##MaintenanceQueue, or ##Results.&nbsp;<br><\/li>\n<\/ul>\n<\/div>\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Users who are powerful in one database can influence tables or modules consumed by code in another database.&nbsp;<\/li>\n<\/ul>\n<\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"h-what-to-monitor-and-how-nbsp\">What to monitor and how&nbsp;<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-server-and-database-configuration\">Server and database configuration<\/h4>\n\n\n\n<p>The server-level option:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT \n    name, \n    value_in_use \nFROM sys.configurations \nWHERE name = N'cross db ownership chaining';<\/pre><\/div>\n\n\n\n<p>Database-level chaining\/trust\/ownership:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT \n    name AS database_name, \n    SUSER_SNAME(owner_sid) AS owner_name, \n    is_db_chaining_on, \n    is_trustworthy_on, \n    is_broker_enabled, \n    is_cdc_enabled \nFROM sys.databases \nWHERE database_id &gt; 4 \n  AND (is_db_chaining_on = 1 OR is_trustworthy_on = 1) \nORDER BY name;<\/pre><\/div>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-shared-owner-review\">Shared owner review<\/h4>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT \n    SUSER_SNAME(owner_sid) AS owner_name, \n    COUNT(*) AS database_count, \n    STRING_AGG(CONVERT(nvarchar(max), name), N', ') AS databases \nFROM sys.databases \nWHERE database_id &gt; 4 \nGROUP BY owner_sid \nHAVING COUNT(*) &gt; 1 \nORDER BY database_count DESC; <\/pre><\/div>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-cross-database-references-in-modules-nbsp\">Cross-database references in modules&nbsp;<\/h4>\n\n\n\n<p>The following simple pattern isn&#8217;t perfect, but is still a useful starting point for finding modules that&nbsp;appear to use&nbsp;three-part names.&nbsp;Combine it with&nbsp;code review and dependency data, and run it in each database:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT \n    DB_NAME() AS database_name, \n    SCHEMA_NAME(o.schema_id) AS schema_name, \n    o.name AS object_name, \n    o.type_desc, \n    o.modify_date, \n    LEFT(m.definition, 4000) AS definition_sample \nFROM sys.sql_modules AS m \nJOIN sys.objects AS o \n    ON o.object_id = m.object_id \nWHERE m.definition LIKE N'%].[%].[%' \n   OR m.definition LIKE N'%..%' \n   OR m.definition LIKE N'%OPENQUERY%' \n   OR m.definition LIKE N'%EXECUTE% AT %' \nORDER BY o.modify_date DESC;<\/pre><\/div>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-global-temporary-objects-used-by-non-admin-sessions-nbsp\">Global temporary objects used by non-admin sessions&nbsp;<\/h4>\n\n\n\n<p>Global temporary object monitoring is usually best handled with Extended Events or a server-side collector. Focus on creation of objects&nbsp;named ##%&nbsp;in&nbsp;<code>tempdb<\/code>, especially near maintenance windows or by unexpected logins.&nbsp;<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">CREATE EVENT SESSION [XE_GlobalTempObjects] \nON SERVER \nADD EVENT sqlserver.object_created \n( \n    ACTION(sqlserver.client_hostname, sqlserver.database_name, \n           sqlserver.server_principal_name, sqlserver.session_id, sqlserver.sql_text) \n    WHERE ([database_name] = N'tempdb') \n), \nADD EVENT sqlserver.object_deleted \n( \n    ACTION(sqlserver.client_hostname, sqlserver.database_name, \n           sqlserver.server_principal_name, sqlserver.session_id, sqlserver.sql_text) \n    WHERE ([database_name] = N'tempdb') \n) \nADD TARGET package0.event_file \n( \n    SET filename = N'D:\\XE\\XE_GlobalTempObjects.xel', \n        max_file_size = 100, \n        max_rollover_files = 10 \n) \nWITH (STARTUP_STATE = ON); \nGO \nALTER EVENT SESSION [XE_GlobalTempObjects] ON SERVER STATE = START; \nGO <\/pre><\/div>\n\n\n\n<p>If your SQL Server version or XE metadata doesn&#8217;t expose object names in the event payload as expected, collect&nbsp;<code>sql_text<\/code>&nbsp;and session context, then correlate them with statements that create ## objects. Test this session in a non-production environment before standardizing it.&nbsp;<\/p>\n\n\n\n<section id=\"my-first-block-block_e99ff65a35cb49754a3cbbd6b1edf233\" 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\">Future-proof database monitoring with Redgate Monitor<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            Multi-platform database observability for your entire estate. Optimize performance, ensure security, and mitigate potential risks with fast deep-dive analysis, intelligent alerting, and AI-powered insights.                                    <\/div>\n            <\/div>\n                                            <a href=\"https:\/\/www.red-gate.com\/products\/redgate-monitor\/\" class=\"btn btn--secondary btn--lg\" aria-label=\"Learn more &amp; try for free: Future-proof database monitoring with Redgate Monitor\">Learn more &amp; try for free<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<h3 class=\"wp-block-heading\" id=\"h-recommended-fixes-and-mitigations-nbsp-2\">Recommended fixes and mitigations&nbsp;<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-keep-cross-database-ownership-chaining-disabled\">Keep cross-database ownership chaining disabled<\/h4>\n\n\n\n<p>The server&nbsp;option&nbsp;should remain off. Database-level <code>DB_CHAINING<\/code> should be an exception with owner approval, documented business justification, and compensating controls.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-stop-sharing-powerful-owners-by-default\">Stop sharing powerful owners by default<\/h4>\n\n\n\n<p>Don&#8217;t make every database owned by&nbsp;<code>sa<\/code>&nbsp;just for convenience. Instead, use dedicated owner logins that aren&#8217;t used for application connections and don&#8217;t hold unnecessary server privileges.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-prefer-module-signing\">Prefer module signing<\/h4>\n\n\n\n<p>When a module in one database must access another database, certificate signing can grant the exact permission to the module without enabling broad cross-database trust.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-use-explicit-permissions-where-possible\">Use explicit permissions where possible<\/h4>\n\n\n\n<p>A clear <code>GRANT<\/code> is easier to review and audit than an implicit ownership chain.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-refactor-global-nbsp-temp-nbsp-object-workflows\">Refactor global&nbsp;temp&nbsp;object workflows<\/h4>\n\n\n\n<p>Privileged jobs should prefer local temporary tables, table variables, or permanent staging tables in locked-down schemas. Avoid predictable ## names for security-sensitive workflows.&nbsp;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-review-after-restores-and-migrations\">Review after restores and migrations<\/h4>\n\n\n\n<p>Restores can introduce owner alignment, <code>DB_CHAINING<\/code>, and cross-database module references that were not present in the target environment before.&nbsp;<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-a-practical-dba-checklist-nbsp-for-you-to-follow\">A practical DBA checklist&nbsp;for you to follow<\/h2>\n\n\n\n<p>The following checklist is intended to be operational. It can be turned into a monthly security review, a health check, or a set of monitoring rules.&nbsp;<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Control<\/strong>&nbsp;<\/td><td><strong>Minimum check<\/strong>&nbsp;<\/td><td><strong>Suggested frequency<\/strong>&nbsp;<\/td><\/tr><tr><td>SQL Agent roles&nbsp;<\/td><td>Review&nbsp;<code>SQLAgentUserRole<\/code>,<br><code>SQLAgentReaderRole<\/code>,&nbsp;and <code>SQLAgentOperatorRole<\/code>&nbsp;<br>membership in&nbsp;<code>msdb<\/code>.&nbsp;<\/td><td>Daily or weekly&nbsp;<\/td><\/tr><tr><td><code>msdb<\/code>&nbsp;privileged roles&nbsp;<\/td><td>Review&nbsp;<code>db_owner<\/code>,&nbsp;<code>db_ddladmin<\/code>,&nbsp;<code>db_securityadmin<\/code>, and explicit <code>ALTER<\/code>\/<code>CONTROL<\/code>\/<code>IMPERSONATE<\/code> permissions.&nbsp;<\/td><td>Weekly&nbsp;<\/td><\/tr><tr><td>Job ownership&nbsp;<\/td><td>List jobs not owned by approved owner accounts; review dangerous subsystems and proxies.&nbsp;<\/td><td>Daily or weekly&nbsp;<\/td><\/tr><tr><td>Job changes&nbsp;<\/td><td>Audit or diff job steps, schedules, owners, proxies, and command text.&nbsp;<\/td><td>Continuous&nbsp;<\/td><\/tr><tr><td>Triggers&nbsp;<\/td><td>Inventory server\/database triggers; alert on create\/alter\/drop; inspect high-risk trigger text.&nbsp;<\/td><td>Continuous plus pre-maintenance&nbsp;<\/td><\/tr><tr><td>Restore activity&nbsp;<\/td><td>Audit <code>BACKUP_RESTORE_GROUP<\/code>;&nbsp;capture source path and operator.&nbsp;<\/td><td>Continuous&nbsp;<\/td><\/tr><tr><td>Post-restore gate&nbsp;<\/td><td>Check&nbsp;<code>owner_sid<\/code>, <code>DB_CHAINING<\/code>, CDC,&nbsp;replication,&nbsp;CLR, triggers, modules, roles.&nbsp;<\/td><td>Every restore&nbsp;<\/td><\/tr><tr><td>Cross-db&nbsp;ownership&nbsp;<\/td><td>Review server&nbsp;option, database <code>DB_CHAINING<\/code>, shared owners, cross-database module references.&nbsp;<\/td><td>Weekly or monthly&nbsp;<\/td><\/tr><tr><td>Global&nbsp;temp&nbsp;workflows&nbsp;<\/td><td>Identify&nbsp;privileged jobs using ## objects or shared&nbsp;<code>tempdb<\/code>&nbsp;coordination.&nbsp;<\/td><td>Monthly and during code review&nbsp;<\/td><\/tr><tr><td>Exceptions&nbsp;<\/td><td>Document every exception with owner, business reason, expiry date, and compensating controls.&nbsp;<\/td><td>Every change&nbsp;<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-conclusion-nbsp\">Conclusion&nbsp;<\/h2>\n\n\n\n<p>The most dangerous SQL Server exposures are often trust-boundary failures rather than specific, isolated bugs. A normal Agent job, a normal restore, a normal trigger, or a normal cross-database module can become an attack path when a lower-trust principal can influence something that higher-trust automation later executes.&nbsp;<\/p>\n\n\n\n<p>For DBAs, the call to action isn&#8217;t to simply disable every feature. Many of them &#8211; such as SQL Server Agent, restores, triggers, CDC, replication, Service Broker, and cross-database modules &#8211; have legitimate uses.<\/p>\n\n\n\n<p>Instead, it&#8217;s about making trust explicit. At all times, you need to know <em>who<\/em> can&nbsp;modify&nbsp;<em>what<\/em>, know which workflows execute under privileged contexts, be aware of audit changes, and avoid allowing untrusted databases or users to become part of privileged automation.&nbsp;<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-summary-the-urgent-actions-you-should-take-to-secure-your-sql-server\">Summary: the urgent actions you should take to secure your SQL Server<\/h3>\n\n\n\n<p><strong><strong>Validate&nbsp;<code>msdb<\/code>, baseline SQL Agent, quarantine untrusted restores, normalize database ownership, disable unnecessary trust properties, inventory triggers, and replace broad ownership chains with explicit, reviewable permissions.<\/strong><\/strong><\/p>\n\n\n\n<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs: MSDB attack paths: how to secure SQL Server Agent, triggers, and restores<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. What is msdb in SQL Server, and why does it matter for security?<\/h3>\n            <div class=\"faq-answer\">\n                <p class=\"font-claude-response-body break-words whitespace-normal\" dir=\"ltr\">Msdb is the system database that stores SQL Server Agent jobs, schedules, proxies, credentials, and backup\/restore history. Because it controls automated, often privileged, execution, a misconfigured or overly permissive msdb can let a low-privileged user influence what a high-privileged job runs.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. How can a SQL Server Agent job be exploited without sysadmin access?<\/h3>\n            <div class=\"faq-answer\">\n                <p class=\"font-claude-response-body break-words whitespace-normal\" dir=\"ltr\">Through indirect execution: a lower-privileged user modifies a job step, called procedure, referenced table, or proxy that a privileged job later runs. The job itself executes the change on the attacker&#8217;s behalf during its next scheduled run.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. Why is restoring a backup from an untrusted source risky?<\/h3>\n            <div class=\"faq-answer\">\n                <p class=\"font-claude-response-body break-words whitespace-normal\" dir=\"ltr\">A backup file contains more than data &#8211; it can include triggers, stored procedures, CLR assemblies, Service Broker configuration, and ownership metadata. Restoring it can introduce executable objects that later run under a privileged context before anyone reviews them.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">4. Can a database trigger really hijack a SQL Server Agent job?<\/h3>\n            <div class=\"faq-answer\">\n                <p class=\"font-claude-response-body break-words whitespace-normal\" dir=\"ltr\">Yes. Many triggers execute in the security context of whoever&#8217;s action fired them. If a lower-privileged user creates a trigger on a table touched by a privileged maintenance job, that trigger can run with the job&#8217;s elevated privileges.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">5. What is cross-database ownership chaining, and why is it a risk?<\/h3>\n            <div class=\"faq-answer\">\n                <p class=\"font-claude-response-body break-words whitespace-normal\" dir=\"ltr\">It&#8217;s a legacy compatibility feature that can bypass permission checks between databases when they share the same owner. If many databases are owned by the same privileged login, access granted in one database can silently extend into another.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">6. What are the first steps to reduce these risks?<\/h3>\n            <div class=\"faq-answer\">\n                <div role=\"feed\" aria-label=\"Chat messages\" aria-describedby=\"_r_ht_\" data-find-provider-scope=\"\">\n<div data-sizer-excess=\"0\" data-rocksteady-sizer=\"\">\n<div data-rs-index=\"1\" data-index=\"1\" data-last-message=\"true\">\n<div role=\"article\" aria-label=\"Message 2 of 2\">\n<div data-test-render-count=\"1\">\n<div class=\"group group\/message-row\">\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 [&amp;_&gt;_*:last-child]:mb-0 print:block print:[&amp;_&gt;_*_+_*]:mt-3 standard-markdown\">\n<p class=\"font-claude-response-body break-words whitespace-normal\" dir=\"ltr\">Restrict SQLAgentUserRole\/ReaderRole\/OperatorRole and db_owner membership in msdb, audit job ownership and proxies, inventory triggers before privileged maintenance, quarantine untrusted restores, and keep cross-database ownership chaining disabled.<\/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<\/div>\n            <\/div>\n            <\/section>\n","protected":false},"excerpt":{"rendered":"<p>Learn how msdb, SQL Server Agent jobs, triggers, and untrusted restores create privilege escalation paths &#8211; plus T-SQL scripts to detect and fix them.&hellip;<\/p>\n","protected":false},"author":65554,"featured_media":107802,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143523,53,143530,46,143524],"tags":[4168,4170,159408,5765,4150,4151],"coauthors":[6809],"class_list":["post-111846","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-database-administration","tag-fasqlsecurity","tag-security-and-compliance","tag-sql","tag-sql-server"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/111846","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=111846"}],"version-history":[{"count":14,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/111846\/revisions"}],"predecessor-version":[{"id":112041,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/111846\/revisions\/112041"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media\/107802"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=111846"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=111846"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=111846"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=111846"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}