powershelldba.de

db_owner Risks: Trigger Creation, Ownership Chaining, and the Path to sysadmin

"Just make it db_owner, it's easier" is one of the most common shortcuts in SQL Server operations. It is also one of the most dangerous. db_owner does not just grant read and write access, it grants the ability to create triggers and procedures that run in the security context of the database owner, and that context frequently leads straight back to sysadmin.

What db_owner Actually Grants

db_owner is not a bundle of convenient permissions. It is functionally equivalent to CONTROL on the database, and its members operate with the same rights as the database owner (dbo) itself. That includes, among other things:

Because db_owner is a fixed, all-encompassing role rather than a set of individually revocable grants, restricting it after the fact does not work. A db_owner member can simply re-grant whatever you took away, or route around a DENY by creating a new object owned by dbo that bypasses the caller's own permission set entirely.

The Real Problem: Ownership Chaining and EXECUTE AS OWNER

SQL Server's ownership chain means that when object A calls object B and both are owned by the same principal, permission checks on B are skipped. A db_owner member can create a procedure or trigger that explicitly runs as the owner:

CREATE PROCEDURE dbo.usp_DoSomethingHarmless
WITH EXECUTE AS OWNER
AS
BEGIN
    -- anything in here runs as dbo, not as the caller
    ...
END;

On its own this is a normal, documented feature used for legitimate impersonation patterns. The problem is who dbo actually is. In most environments, databases are created by a setup script, a migration tool, or an admin running as themselves, which means the database owner is frequently a login that already holds sysadmin at the server level, not a scoped, database-only account.

⚠ The escalation path: if the database has TRUSTWORTHY set to ON and the database owner maps to a sysadmin login, any db_owner member can create an object with EXECUTE AS OWNER that reaches outside the database entirely, for example adding themselves to the sysadmin server role. The impersonation that was meant to stay inside one database crosses the trust boundary because TRUSTWORTHY tells SQL Server to honor it at the instance level too.

What This Looks Like in Practice

-- Check the actual exposure on an instance
SELECT
    d.name                                   AS database_name,
    d.is_trustworthy_on,
    SUSER_SNAME(d.owner_sid)                 AS db_owner_login,
    IS_SRVROLEMEMBER('sysadmin', SUSER_SNAME(d.owner_sid)) AS owner_is_sysadmin
FROM sys.databases d
WHERE d.database_id > 4
ORDER BY d.is_trustworthy_on DESC, owner_is_sysadmin DESC;

Any row where is_trustworthy_on = 1 and owner_is_sysadmin = 1 is a database where every db_owner member is, in effect, one CREATE PROCEDURE ... WITH EXECUTE AS OWNER statement away from full instance control.

Triggers Make It Worse, Not Better

A malicious or careless db_owner member does not even need to call a procedure explicitly. A DML or DDL trigger with EXECUTE AS OWNER fires automatically on ordinary application activity, which means the elevated code path executes silently as part of routine INSERT, UPDATE, or schema-change traffic. It requires no special privilege on the caller's side to trigger, it leaves no obvious "someone ran a suspicious procedure" trail, and it survives long after whoever created it has moved on. This is precisely why db_owner is a poor fit for anyone other than the small number of people who are supposed to be able to redesign the schema: the same rights that let a legitimate developer ship a migration also let any db_owner member, malicious or compromised, plant a durable backdoor.

Why DENY and Auditing Do Not Save You Here

Because db_owner includes the right to manage permissions on the database, a member can:

Least privilege only works when the role you hand out cannot rewrite the rules it is being judged against. db_owner can.

The Fix: db_datareader, db_datawriter, and a Custom db_execute Role

Almost nobody actually needs db_owner. Applications need to read and write data and call the procedures they were built against. Developers, in the overwhelming majority of day-to-day work, need the same thing. Schema changes should go through a controlled deployment path, not through a role that is active every time the application connects.

Role Grants Can create triggers/procedures?
db_owner Everything (equivalent to CONTROL on the database) Yes, and can run them as dbo
db_datareader SELECT on all tables and views No
db_datawriter INSERT, UPDATE, DELETE on all tables No
db_execute (custom) EXECUTE on procedures/functions in a defined schema No, membership alone grants no DDL rights

db_execute is not a built-in SQL Server role. You create it once per database and grant it exactly what your application actually calls:

CREATE ROLE db_execute AUTHORIZATION dbo;
GRANT EXECUTE ON SCHEMA::dbo TO db_execute;

-- add the application login / service account
ALTER ROLE db_datareader ADD MEMBER app_service_account;
ALTER ROLE db_datawriter ADD MEMBER app_service_account;
ALTER ROLE db_execute    ADD MEMBER app_service_account;

-- remove the shortcut
ALTER ROLE db_owner DROP MEMBER app_service_account;
✓ Why this combination is safe against the escalation above: none of these three roles include permission to create, alter, or drop procedures and triggers, none include permission to manage other principals' permissions, and none can touch TRUSTWORTHY or server-level roles. There is no EXECUTE AS OWNER object for a db_datareader/db_datawriter/db_execute member to create in the first place.

If a schema for a single procedure needs finer control than "everything in the schema," grant EXECUTE object by object instead of at the schema level, or move genuinely sensitive procedures into a separate schema that db_execute is not granted on.

Where Real DDL Rights Still Belong

Someone still has to deploy schema changes. That does not mean the application account, or every developer's personal login, needs standing db_owner. Keep DDL rights on a small number of scoped deployment identities, grant them only for the duration of a release where that is practical, and prefer signed procedures or a certificate-based impersonation pattern over blanket ownership when a specific operation genuinely needs elevated rights inside the database. The goal is the same one behind any least-privilege model: the permission should exist only when it is being used, not permanently, "just in case."

Migration Checklist

  1. Inventory current db_owner members per database with sys.database_role_members or sp_helprolemember 'db_owner'
  2. Run the is_trustworthy_on / owner_is_sysadmin query above across the instance and treat any positive hit as a priority
  3. Create the db_execute role per database and grant it against the schema(s) the application actually calls
  4. Add affected logins to db_datareader, db_datawriter, and db_execute; test the application against this set in a non-production environment first
  5. Watch for DDL hidden inside application code paths (auto-migrations, "create table if not exists" logic); these need a separate, scoped deployment identity, not a return to db_owner
  6. Remove db_owner membership once testing confirms the application has no remaining dependency on it
  7. Set TRUSTWORTHY OFF unless there is a documented, still-necessary reason for it being on

Integration with Control Matrix

Our Control Matrix covers least-privilege access control across SQL Server infrastructure in more depth. See also GRANT, DENY, REVOKE: Understanding SQL Server Permissions for how permission inheritance and precedence work at the object level.

The Bottom Line

db_owner is not "read/write plus a bit extra." It is full control over the database, including the ability to create code that runs as the database owner and, depending on TRUSTWORTHY and who that owner actually is, potentially as sysadmin on the instance. It cannot be safely restricted after the fact because it includes the right to undo any restriction placed on it.

For the vast majority of application accounts and day-to-day development work, db_datareader, db_datawriter, and a purpose-built db_execute role provide everything that is actually needed, and none of what makes db_owner dangerous.

← Back to Blog