{"id":112430,"date":"2026-08-28T12:00:00","date_gmt":"2026-08-28T12:00:00","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=112430"},"modified":"2026-08-28T12:28:15","modified_gmt":"2026-08-28T12:28:15","slug":"why-sql-servers-default-trigger-execution-model-is-a-security-flaw-and-what-microsoft-should-do-about-it","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/why-sql-servers-default-trigger-execution-model-is-a-security-flaw-and-what-microsoft-should-do-about-it\/","title":{"rendered":"Why SQL Server&#8217;s default trigger execution model is a security flaw &#8211; and what Microsoft should do about it"},"content":{"rendered":"\n<p><strong><a href=\"https:\/\/www.red-gate.com\/simple-talk\/data-security-privacy-compliance\/how-azure-arc-allows-a-db-admin-to-become-sql-server-sysadmin-the-vulnerability-explained\/\" target=\"_blank\" rel=\"noreferrer noopener\">In my previous article<\/a>, I described a privilege escalation scenario involving Azure Arc-enabled SQL Server and a database-level DDL trigger created by a database-scoped principal. This database-level DDL trigger was later fired by a highly-privileged Azure Arc operation.<\/strong><\/p>\n\n\n\n<p><strong>The situation raises a question I\u2019ve been thinking for a while: why does SQL Server allow trigger code to inherit the privileges of <em>whichever principal happens to fire it?<\/em><\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-problem-trigger-code-inherits-the-caller-s-privileges\">The problem: trigger code inherits the caller&#8217;s privileges<\/h2>\n\n\n\n<p><strong>By default, SQL Server executes both <a href=\"https:\/\/www.geeksforgeeks.org\/dbms\/difference-between-ddl-and-dml-in-dbms\/\" target=\"_blank\" rel=\"noreferrer noopener\">DML (data manipulation language) and DDL (data definition language)<\/a> triggers under the security context of the user whose statement causes the trigger to fire.<\/strong> <strong>The trigger author supplies the code, but the future caller supplies the privileges under which that code runs.<\/strong><\/p>\n\n\n\n<p>This is the default behavior of <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\">SQL Server triggers<\/a>, and it creates a dangerous situation. One principal controls the trigger code, another supplies the execution privileges and, combined, the trigger can then potentially hijack the caller&#8217;s authority.<\/p>\n\n\n\n<p>Additionally, a user who creates a trigger does not need permission to perform every operation contained in the trigger. They only need permission to <em>create<\/em> the trigger &#8211; and then an opportunity for a more privileged principal to fire it later. <\/p>\n\n\n\n<p>This is not an undocumented side effect of SQL Server &#8211; in fact, it&#8217;s the <em>documented<\/em> default <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/performance\/execution-plans?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">execution<\/a> model. Microsoft even explicitly warns that this behavior can be exploited to introduce malicious code that later executes under elevated privileges. <\/p>\n\n\n\n<p>So, the question is: <em>why should this be allowed to happen?<\/em> Is this <em>really<\/em> the best approach?<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The trigger author writes the code, but the caller supplies the privileges<\/h2>\n\n\n\n<p>Consider the security relationship created by a trigger. One principal creates or modifies executable code, which remains stored inside the database. Then, later on, another principal performs an operation that causes SQL Server to execute it automatically.<\/p>\n\n\n\n<p>Those two principals may have completely different levels of trust. The trigger author might be an application developer with permissions over a particular table, a database schema administrator, a deployment account, or a principal that can create database-level DDL triggers. <\/p>\n\n\n\n<p>The future caller, meanwhile, might be a <a href=\"https:\/\/www.red-gate.com\/simple-talk\/career\/what-does-a-modern-dba-actually-do-6-key-points-from-a-senior-dba\/\" target=\"_blank\" rel=\"noreferrer noopener\">DBA<\/a>, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/msdb-attack-paths-how-to-secure-sql-server-agent-triggers-and-restores\/\" target=\"_blank\" rel=\"noreferrer noopener\">SQL Server Agent<\/a>, a deployment system, <a href=\"https:\/\/www.red-gate.com\/products\/redgate-monitor\/\" target=\"_blank\" rel=\"noreferrer noopener\">monitoring software<\/a>, a privileged service account, a cloud-management extension, or a member of <code>sysadmin<\/code>.<\/p>\n\n\n\n<p><strong>The key here is that, unless an execution context is explicitly specified, SQL Server makes a critical security decision <em>automatically<\/em>: the stored code executes as the caller.<\/strong> <strong>The person supplying the code and the person supplying the privileges are therefore not necessarily the same person<\/strong> <strong>&#8211; and the privilege available to the code can change depending entirely on <em>who<\/em> happens to activate it.<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-how-does-this-differ-from-ordinary-sql-server-stored-procedures\">How does this differ from ordinary SQL Server stored procedures?<\/h2>\n\n\n\n<p><code>CALLER<\/code> <em>can<\/em> make sense for an ordinary <a href=\"https:\/\/www.red-gate.com\/simple-talk\/other\/for-the-love-of-stored-procedures\/\" target=\"_blank\" rel=\"noreferrer noopener\">stored procedure<\/a>. A user deliberately invokes a module, and there&#8217;s a relatively obvious relationship between the caller and the code being executed:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">EXEC dbo.DoSomething;<\/pre><\/div>\n\n\n\n<p>Triggers, on the other hand, are different. A trigger is an event handler, so the caller doesn&#8217;t explicitly execute it. It&#8217;s therefore possible for a user to run a legitimate DDL statement and unknowingly activate additional code:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">ALTER TABLE dbo.Customer ADD NewColumn int;<\/pre><\/div>\n\n\n\n<p>The principal performing the original operation may not even know the trigger exists, especially if they weren&#8217;t responsible for its creation (and, in that case, they won&#8217;t have control of its source code either). Nevertheless, the default model can allow that stored code to consume the caller&#8217;s authority.<\/p>\n\n\n\n<section id=\"my-first-block-block_a75d78c5306a7d948ce68238e12ec4ef\" 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-why-event-driven-code-deserves-a-different-trust-model\">Why event-driven code deserves a different trust model<\/h2>\n\n\n\n<p>For <a href=\"https:\/\/en.wikipedia.org\/wiki\/Event-driven_architecture\" target=\"_blank\" rel=\"noreferrer noopener\">event-driven<\/a> execution, that trust relationship is worth challenging. The privileges available to stored executable code should normally be determined by the code&#8217;s configured identity, not by whichever unrelated principal happens to generate the event.<\/p>\n\n\n\n<p>Other major database engines have made different architectural choices. For example, <a href=\"https:\/\/docs.oracle.com\/en\/database\/oracle\/oracle-database\/26\/lnpls\/triggers-publishing-events.html\" target=\"_blank\" rel=\"noreferrer noopener\">Oracle&#8217;s documentation<\/a> states that a trigger always behaves as a <code>definer-rights<\/code> unit. The trigger therefore executes according to the security context associated with its definer rather than simply inheriting the privileges of the user whose action caused it to fire.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-sql-server-already-has-a-safer-model-in-service-broker\">SQL Server already has a safer model in Service Broker<\/h2>\n\n\n\n<p><strong><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/database-engine\/configure-windows\/sql-server-service-broker?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">SQL Server&#8217;s Service Broker<\/a> provides a useful comparison as it can automatically start a stored procedure when messages arrive in a queue. <\/strong><\/p>\n\n\n\n<p>Conceptually, it&#8217;s another event-driven execution mechanism: an event occurs, SQL Server automatically invokes stored code, and the code processes the event.<\/p>\n\n\n\n<div id=\"callout-block_e1123833adb1a3c2c36a473ba6eb6e0c\" 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>You may also be interested in:<\/strong><\/p>\n\n\n\n<p><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/learn\/service-broker-foundations-workbench\/\" target=\"_blank\" rel=\"noreferrer noopener\">Service Broker Foundations Workbench<\/a><\/p>\n\n\n\n<p><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 Advanced Basics Workbench<\/a><\/p>\n\n<\/div>\n<\/div> \n\n\n<p>However, Service Broker doesn&#8217;t just execute the activation procedure as the user who sent the message. Instead, internally-activated procedures run in a background session distinct from the connection that created the message. The execution identity is associated with the queue\u2019s activation configuration, rather than borrowed from the sender.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-execute-as-self-vs-caller-in-queue-activation\"><code>EXECUTE AS SELF<\/code> vs. <code>CALLER<\/code> in queue activation<\/h3>\n\n\n\n<p>When configuring queue activation, SQL Server supports an explicit <code>EXECUTE AS<\/code> identity:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">CREATE QUEUE ExpenseQueue\nWITH ACTIVATION\n(\n    PROCEDURE_NAME = dbo.ProcessExpense,\n    MAX_QUEUE_READERS = 5,\n    EXECUTE AS { SELF | 'user_name' | OWNER }\n);\nGO<\/pre><\/div>\n\n\n\n<p>Microsoft states that the specified database user is the account under which the activation procedure runs. The sender of the message doesn&#8217;t automatically donate their security token to the activated procedure. The broader <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\"><code>EXECUTE AS<\/code> documentation<\/a> makes the difference even clearer:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><code>CALLER<\/code> is the default for most modules, including triggers, and can&#8217;t be specified for queues.<\/li>\n\n\n\n<li><code>SELF<\/code> is the default execution identity for queues. It represents the principal that created or last altered the module, whose <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/functions\/database-principal-id-transact-sql?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">principal ID<\/a> is stored in metadata.<\/li>\n<\/ul>\n<\/div>\n\n\n<p>Service Broker therefore separates three security identities: the principal sending the message, the principal configured to run the activation procedure, and any additional execution identity explicitly declared by the stored procedure.<\/p>\n\n\n\n<p>The sender controls the event and its message content, but doesn&#8217;t automatically control the privileges used to <em>process<\/em> that event. Triggers should, in my opinion, follow the same security principle.<\/p>\n\n\n\n<p>So, while Service Broker is not directly equivalent to DDL triggers &#8211; with different execution semantics and compatibility requirements &#8211; it <em>does<\/em> illustrate a security design principle relevant to this discussion. <\/p>\n\n\n\n<p>That being:<strong> event-driven code in SQL Server can execute under an explicitly configured identity rather than automatically inheriting the security token of the principal that caused the event.<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-what-would-a-safer-sql-server-trigger-default-look-like\">What would a safer SQL Server trigger default look like?<\/h2>\n\n\n\n<p><strong>The execution context of a SQL Server trigger <em>should<\/em> be determined when the trigger is created or altered, not inherited unpredictably from each future caller.<\/strong><\/p>\n\n\n\n<p>At minimum, the default should be equivalent to:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">CREATE TRIGGER trg_Example\nON DATABASE\nWITH EXECUTE AS SELF\nFOR DDL_DATABASE_LEVEL_EVENTS\nAS\nBEGIN\n    -- Trigger body\nEND;<\/pre><\/div>\n\n\n\n<p><code>EXECUTE AS SELF<\/code> means the trigger runs as the principal that created or last altered it. SQL Server stores that principal in the module metadata. Under this model, a login with <code>db_ddladmin<\/code> could still create a trigger, but the trigger would run only with the creator\u2019s database-level privileges. <\/p>\n\n\n\n<p>This ensures that the trigger doesn&#8217;t suddenly become more powerful because a <code>sysadmin<\/code>, <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/azure-arc\/overview\" target=\"_blank\" rel=\"noreferrer noopener\">Azure Arc<\/a>, or another privileged service fired it. It&#8217;s also a <em>predictable<\/em> security behavior: the low-privileged principal creates the trigger, runs as low-privileged principal, and the server-level operation remains denied.<\/p>\n\n\n\n<p>A trusted administrator who deliberately needs a trigger to run under another identity could explicitly configure that identity:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">WITH EXECUTE AS SELF<\/pre><\/div>\n\n\n\n<p>or:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">WITH EXECUTE AS 'DedicatedTriggerUser'<\/pre><\/div>\n\n\n\n<p>&#8230;and the decision to grant the trigger additional authority would then be explicit, reviewable, and visible in metadata. Today, however, the dangerous choice is implicit.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-why-requiring-an-explicit-execution-context-would-be-even-better\">Why requiring an explicit execution context would be even better<\/h2>\n\n\n\n<p>Changing the default from <code>CALLER<\/code> to <code>SELF<\/code> would substantially reduce the risk, but the strongest design would be to require trigger creators to declare an execution context explicitly.<\/p>\n\n\n\n<p>For example:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">CREATE TRIGGER trg_Example\nON DATABASE\nWITH EXECUTE AS SELF\nFOR CREATE_TABLE, ALTER_TABLE\nAS\nBEGIN\n    -- Trigger body\nEND;<\/pre><\/div>\n\n\n\n<p>Or, when legacy caller behavior is genuinely required:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">CREATE TRIGGER trg_Example\nON DATABASE\nWITH EXECUTE AS CALLER\nFOR CREATE_TABLE, ALTER_TABLE\nAS\nBEGIN\n    -- Trigger body\nEND;<\/pre><\/div>\n\n\n\n<p>Requiring the clause would force the author to make a conscious security decision, and would also make security reviews easier. An auditor could immediately distinguish triggers intentionally configured to use the caller\u2019s token from triggers constrained to a fixed database principal.<\/p>\n\n\n\n<div id=\"callout-block_e1123833adb1a3c2c36a473ba6eb6e0c\" 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>You may also be interested in:<\/strong><\/p>\n\n\n\n<p><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\u2019s complete guide to SQL Server security<\/a><\/p>\n\n<\/div>\n<\/div> \n\n\n<p><code>EXECUTE AS CALLER<\/code> should be an explicit opt-in to a dangerous behavior, not an invisible default inherited by every trigger that omits the clause.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-why-is-an-explicit-choice-better-than-a-hidden-default\">Why is an explicit choice better than a hidden default?<\/h2>\n\n\n\n<p>If a trigger explicitly contains <code>WITH EXECUTE AS CALLER<\/code>, the trust relationship becomes obvious. A DBA or security reviewer can ask:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Who might fire this trigger?<br><br><\/li>\n\n\n\n<li><em>Could<\/em> a sysadmin fire it?<br><br><\/li>\n\n\n\n<li><em>Could<\/em> SQL Server Agent, a deployment system, or a management extension fire it?<br><br><\/li>\n\n\n\n<li>Does the trigger contain <a href=\"https:\/\/www.sqlservercentral.com\/articles\/dos-and-donts-of-dynamic-sql\" target=\"_blank\" rel=\"noreferrer noopener\">dynamic SQL<\/a> or cross-database references?<br><br><\/li>\n\n\n\n<li>Can it change <a href=\"https:\/\/www.red-gate.com\/simple-talk\/data-security-privacy-compliance\/data-privacy-and-protection\/introduction-to-sql-server-security-part-2\/#digging-into-permissions:~:text=to%20that%20user.-,Digging%20into%20Permissions,-Several%20of%20the\" target=\"_blank\" rel=\"noreferrer noopener\">permissions<\/a>, logins, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/database-administration-sql-server\/sql-server-security-fixed-server-and-database-roles\/\" target=\"_blank\" rel=\"noreferrer noopener\">roles<\/a>, or server configuration when executed by a privileged login?<br><br><\/li>\n\n\n\n<li>Does the trigger need caller semantics at all?<\/li>\n<\/ul>\n<\/div>\n\n\n<p>When <code>CALLER<\/code> is simply the <em>consequence<\/em> of omitting an <code>EXECUTE AS<\/code> clause, that security decision is easier to miss.<\/p>\n\n\n\n<section id=\"my-first-block-block_a5fa991c0211189589e71ae5395fb8fb\" 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 now: Subscribe to the Simple Talk newsletter\">Subscribe now<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<h2 class=\"wp-block-heading\" id=\"h-addressing-sql-server-backward-compatibility\">Addressing SQL Server backward compatibility<\/h2>\n\n\n\n<p>One obvious objection to this: SQL Server has behaved this way for many years, and existing triggers may intentionally depend on caller context. <\/p>\n\n\n\n<p>A trigger might inspect the caller, depend on caller permissions when accessing another object, implement auditing logic based on execution identity, or intentionally produce different behavior for different users. And I do agree that Microsoft can&#8217;t just (safely) change every existing trigger in a cumulative update.<\/p>\n\n\n\n<p>However, backward compatibility and permanent preservation of an unsafe default are <em>not<\/em> the same thing. Some possible transition mechanisms include:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Preserve <code>CALLER<\/code> for existing databases while using <code>SELF<\/code> for newly created databases.<br><br><\/li>\n\n\n\n<li>Introduce a database-scoped configuration controlling the default trigger context.<br><br><\/li>\n\n\n\n<li>Tie the safer behavior to a future database compatibility level.<br><br><\/li>\n\n\n\n<li>Require an explicit execution context for newly-created triggers.<br><br><\/li>\n\n\n\n<li>Generate a warning when <code>CREATE TRIGGER<\/code> omits <code>EXECUTE AS<\/code>.<br><br><\/li>\n\n\n\n<li>Add security assessment rules that identify triggers running as <code>CALLER<\/code>.<br><br><\/li>\n\n\n\n<li>Warn when a principal with database-level trigger permissions can create code that may be invoked by a server-privileged account.<br><br><\/li>\n\n\n\n<li>Add an <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 Event<\/a> or audit event when a trigger runs under a context <em>more<\/em> privileged than its creator.<br><br><\/li>\n\n\n\n<li>Prevent database-level triggers running as <code>CALLER<\/code> from inheriting server-level permissions unless explicitly authorized.<\/li>\n<\/ul>\n<\/div>\n\n\n<p>Microsoft has successfully introduced compatibility levels and opt-in security improvements for many other SQL Server behaviors. Trigger execution context should receive the same treatment.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-why-microsoft-s-current-guidance-puts-the-burden-on-the-wrong-party\">Why Microsoft\u2019s current guidance puts the burden on the wrong party<\/h2>\n\n\n\n<p>Microsoft\u2019s current trigger-security guidance advises administrators to review triggers and disable those that could execute under elevated privileges. Useful operational advice, yes, but it&#8217;s just treating the symptom rather than the design problem:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Every privileged caller is expected to determine whether hostile triggers exist before performing DDL.<br><br><\/li>\n\n\n\n<li>Every service is expected to understand that an apparently ordinary statement may execute attacker-controlled code with the service\u2019s complete authority.<br><br><\/li>\n\n\n\n<li>Every administrator is expected to audit every trigger created by every database principal <em>before<\/em> performing <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/how-sql-server-maintenance-becomes-an-attack-path-and-what-to-do-about-it\/\" target=\"_blank\" rel=\"noreferrer noopener\">routine maintenance<\/a>.<\/li>\n<\/ul>\n<\/div>\n\n\n<p>Put simply, this security model just doesn&#8217;t scale. A safer platform would have stored code constrained by default, eliminating the need for every future caller to defend its privileges against code already present in the database.<\/p>\n\n\n\n<p>The party that creates executable code should define &#8211; or be constrained by &#8211; the identity under which that code executes. And, a trigger should execute under a deliberate, stable, least-privileged identity &#8211; not automatically inherit the authority of whichever user, administrator, agent, extension, or cloud service happens to activate it.<\/p>\n\n\n\n<p>Service Broker demonstrates that SQL Server <em>can<\/em> implement event-driven execution without lending the sender\u2019s privileges to the receiving code. Triggers should work the same way.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-final-thoughts-why-sql-server-privilege-should-be-explicit-stable-and-reviewable\">Final thoughts: why SQL Server privilege should be explicit, stable, and reviewable<\/h2>\n\n\n\n<p>SQL Server&#8217;s trigger engine is behaving as documented. The question I have is whether the documented default remains a good security design.<\/p>\n\n\n\n<p>A trigger is persistent executable code that can be written by one principal, yet activated later by a completely unrelated principal with substantially greater authority. With the default <code>CALLER<\/code> model, the trigger&#8217;s effective privilege is therefore <em>not<\/em> fixed when the code is created. Instead, it purely depends on <em>who<\/em> happens to activate it. <\/p>\n\n\n\n<p>This makes the security properties of the code difficult to make sense of. The trigger has simply been given a more powerful execution token, so a database-scoped trigger that is harmless when fired by its creator can suddenly become dangerous when fired by a <code>sysadmin<\/code>. This should <em>not<\/em> be the case. <\/p>\n\n\n\n<p><strong>Instead, the privileges of stored executable code in SQL Server should instead be explicit, stable, and reviewable. After all, SQL Server already provides mechanisms to specify an execution context. <\/strong><\/p>\n\n\n\n<p><strong>What deserves reconsideration is the default. <code>EXECUTE AS CALLER<\/code> should be a <em>deliberate choice<\/em> for trigger authors who genuinely require caller semantics &#8211; not an invisible security decision made automatically whenever the author says nothing.<\/strong><\/p>\n\n\n\n<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. What is the default execution context for SQL Server triggers?<\/h3>\n            <div class=\"faq-answer\">\n                <p dir=\"ltr\">By default, both DML and DDL triggers execute under <code>EXECUTE AS CALLER<\/code> \u2014 meaning the trigger runs with the privileges of whoever&#8217;s action fired it, not the privileges of the person who wrote the trigger.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. Why is EXECUTE AS CALLER a security risk for triggers?<\/h3>\n            <div class=\"faq-answer\">\n                <p dir=\"ltr\">Because a low-privileged user can write trigger code that later executes with the full authority of a higher-privileged principal \u2014 such as a sysadmin, SQL Server Agent, or a cloud management extension \u2014 simply by getting that principal to perform an ordinary DDL or DML operation.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. How does Oracle handle trigger execution differently from SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p dir=\"ltr\">Oracle documentation specifies that triggers are always definer-rights units, meaning they execute under the security context of whoever defined the trigger \u2014 not the user whose action caused it to fire.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">4. Can you change a SQL Server trigger to use a fixed execution identity?<\/h3>\n            <div class=\"faq-answer\">\n                <p dir=\"ltr\">Yes. Adding <code>WITH EXECUTE AS SELF<\/code> (or a named user) to a <code>CREATE TRIGGER<\/code> statement makes the trigger run under a stable, creator-defined identity instead of inheriting the caller&#8217;s privileges \u2014 though this isn&#8217;t the default and must be set explicitly.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">5. How does Service Broker avoid this privilege inheritance problem?<\/h3>\n            <div class=\"faq-answer\">\n                <p dir=\"ltr\">Service Broker&#8217;s queue activation procedures don&#8217;t run as the message sender by default \u2014 they use <code>EXECUTE AS SELF<\/code> by default and support an explicit <code>EXECUTE AS<\/code> clause, separating the sender&#8217;s identity from the identity used to process the event.<\/p>\n            <\/div>\n            <\/section>\n\n\n\n<h2 class=\"wp-block-heading\">References<\/h2>\n\n\n\n<p>1. <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/triggers\/manage-trigger-security?view=sql-server-ver17\">Microsoft Learn &#8211; Manage trigger security<\/a><\/p>\n\n\n\n<p>2. <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/statements\/create-trigger-transact-sql?view=sql-server-ver17\">Microsoft Learn &#8211; CREATE TRIGGER (Transact-SQL)<\/a><\/p>\n\n\n\n<p>3. <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/statements\/execute-as-clause-transact-sql?view=sql-server-ver17\">Microsoft Learn &#8211; EXECUTE AS clause (Transact-SQL)<\/a><\/p>\n\n\n\n<p>4. <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/statements\/execute-as-transact-sql?view=sql-server-ver17\">Microsoft Learn &#8211; EXECUTE AS (Transact-SQL)<\/a><\/p>\n\n\n\n<p>5. <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/statements\/create-queue-transact-sql?view=sql-server-ver17\">Microsoft Learn &#8211; CREATE QUEUE (Transact-SQL)<\/a><\/p>\n\n\n\n<p>6. <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/security\/trustworthy-database-property?view=sql-server-ver17\">Microsoft Learn &#8211; TRUSTWORTHY database property<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>SQL Server triggers execute under the caller&#8217;s privileges by default &#8211; not the creator&#8217;s. Learn why this design enables privilege escalation, how Oracle and Service Broker do it differently, and what a safer EXECUTE AS default would look like.&hellip;<\/p>\n","protected":false},"author":65554,"featured_media":108032,"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,4619,5765,4150,4151,159394],"coauthors":[6809],"class_list":["post-112430","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-security","tag-security-and-compliance","tag-sql","tag-sql-server","tag-sql-server-security-vulnerabilities"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/112430","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=112430"}],"version-history":[{"count":13,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/112430\/revisions"}],"predecessor-version":[{"id":112670,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/112430\/revisions\/112670"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media\/108032"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=112430"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=112430"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=112430"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=112430"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}