This article takes one practical thread from Fabiano Amorim’s broader SQL Server security research: how apparently normal permissions and routine maintenance can become an attack path. The goal is not to walk through a full proof of concept, but to help DBAs answer a more useful production question: how do I know whether my SQL Server instances are exposed to this kind of risk?
The article covers four connected areas: over-privileged database users, msdb and SQL Server Agent exposure, privileged maintenance jobs, and trigger-based permission hijacking.
For each area, we will look at why the risk exists, what signs a DBA should look for, what specifically to monitor, and how to mitigate the exposure.
This article is part of Fabiano Amorim’s complete guide to SQL Server security. However, the article also takes learnings from Fabiano’s other series, in which he uncovers SQL Server vulnerabilities you may not be aware of.
SQL Server – secure by default?
SQL Server is often marketed or perceived as secure by default, but real-world testing shows that default installations and default permissions still expose several attack surfaces. With this in mind, “secure by default” does not mean “safe without review.”
Many risks come from legacy behavior, backward compatibility, broad permissions granted to the public role, system procedures, implicit trust boundaries, and administrative convenience features.
The risk appears when they are combined: a database user with more permissions than necessary, a maintenance job running as a highly privileged owner, an unexpected trigger, or an impersonated module that changes how permissions are evaluated.
The attack path explained
Real SQL Server security incidents rarely require one catastrophic bug. They often come from chaining smaller weaknesses that were each accepted as normal operational practice.
A simplified chain looks like this:
- An application login, support login, or database user is granted
db_owner,db_ddladmin,db_securityadmin, or broadALTERpermissions in a production database.
- That principal can create or modify objects such as stored procedures, DDL (data definition language) triggers, DML (data manipulation language) triggers, or modules that execute as dbo or owner.
- The trigger or module runs during that trusted operation and inherits the permissions of the caller that fired it.
- The result is privilege escalation, audit evasion, unwanted configuration changes, or unauthorized data access.
The important point is that none of these items may look suspicious when reviewed in isolation. DBAs create maintenance jobs. Applications sometimes need deployments. SQL Agent often runs administrative workflows. Triggers are a normal SQL Server feature. The security problem is the trust boundary between them.
Over-privileged database users in SQL Server: why (and how) normal permissions can become dangerous
The db_owner fixed database role is frequently treated as a convenient application role. It is not. A db_owner member controls the database security boundary.
The user can create and alter objects, define triggers, change permissions, create modules with elevated execution context, and often influence operations performed later by privileged users or automated jobs.
The same concern applies to db_ddladmin, db_securityadmin, db_accessadmin, CONTROL DATABASE, ALTER ANY USER, ALTER ANY ROLE, ALTER ANY SCHEMA, and broad ALTER permissions on important schemas.
These rights may be necessary for controlled deployment accounts, but they are usually too powerful for runtime application accounts and permanent support accounts.
What are the warning signs a DBA should look for?
- Application service accounts, reporting users, vendor users, or generic support logins in
db_owner.
- Permanent membership in
db_ddladmin,db_securityadmin, ordb_accessadmin.
- Runtime application accounts that also perform schema deployments.
- Users granted
CONTROL DATABASE,ALTER ANY SCHEMA,ALTER ANY USER,ALTER ANY ROLE, orIMPERSONATE.
- Databases where
TRUSTWORTHYis ON, cross-database ownership chaining is enabled, or the owner is a sysadmin login.
- Unexpected created or modified procedures, triggers, views, synonyms, or assemblies in production.
Protect your data. Demonstrate compliance.
What to monitor, and how
| Area | What to check | How to monitor |
| Role membership | Members of: db_ownerdb_ddladmin | Daily catalog checks and SQL Audit DATABASE_ROLE_MEMBER_CHANGE_GROUP. Alert on changes outside approved deployment windows. |
| Database permissions | CONTROL DATABASEALTER ANY ROLEIMPERSONATECREATE ASSEMBLYCREATE PROCEDURECREATE TRIGGER | Query sys.database_permissions in every user database. Baseline expected permissions and diff against that baseline. |
| Risky database settings | TRUSTWORTHYDB_CHAININGowner_sidService Broker activation procedures CLR settings | Query sys.databases and database scoped metadata. Treat deviations as configuration drift. |
| Object change activity | CREATE, ALTER, and DROP activity for modules, triggers, schemas, users, roles, and assemblies. | SQL Audit DATABASE_OBJECT_CHANGE_GROUPSCHEMA_OBJECT_CHANGE_GROUPDATABASE_PRINCIPAL_CHANGE_GROUPDATABASE_PERMISSION_CHANGE_GROUPUse DDL triggers only for logging, not as the only control. |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* High-risk database role membership in the current database */ SELECT DB_NAME() AS database_name, role_principal.name AS role_name, member_principal.name AS member_name, member_principal.type_desc AS member_type FROM sys.database_role_members AS drm JOIN sys.database_principals AS role_principal ON drm.role_principal_id = role_principal.principal_id JOIN sys.database_principals AS member_principal ON drm.member_principal_id = member_principal.principal_id WHERE role_principal.name IN ( N'db_owner', N'db_ddladmin', N'db_securityadmin', N'db_accessadmin', N'db_backupoperator' ) ORDER BY role_principal.name, member_principal.name; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* Database-level permissions that deserve review */ SELECT DB_NAME() AS database_name, USER_NAME(dp.grantee_principal_id) AS grantee_name, dp.state_desc, dp.permission_name, dp.class_desc, OBJECT_SCHEMA_NAME(dp.major_id) AS securable_schema, OBJECT_NAME(dp.major_id) AS securable_name FROM sys.database_permissions AS dp WHERE dp.permission_name IN ( N'CONTROL', N'ALTER', N'ALTER ANY SCHEMA', N'ALTER ANY USER', N'ALTER ANY ROLE', N'IMPERSONATE', N'CREATE PROCEDURE', N'CREATE FUNCTION', N'CREATE TRIGGER', N'CREATE ASSEMBLY' ) ORDER BY grantee_name, permission_name; |
|
1 2 3 4 5 6 7 8 9 10 |
/* Instance-level database settings that affect trust boundaries */ SELECT name AS database_name, SUSER_SNAME(owner_sid) AS database_owner, is_trustworthy_on, is_db_chaining_on, containment_desc FROM sys.databases WHERE database_id > 4 ORDER BY name; |
Recommended fixes and mitigations
- Separate deployment permissions from runtime permissions. The account that deploys schema changes should not be the same account used by the application during normal operations.
- Replace
db_ownerwith custom roles that grant only the requiredSELECT,INSERT,UPDATE,DELETE, andEXECUTEpermissions.
- Make elevated access temporary, approved, and auditable. Remove it after the deployment or support task is complete.
- Keep
TRUSTWORTHYOFF unless there is a documented, reviewed, and tested requirement. Do not useTRUSTWORTHYas a shortcut for cross-database access.
- Use dedicated low-use database owner logins. Avoid making user databases owned by sa, domain administrators, or application runtime accounts.
- Build a permission baseline and alert on drift, rather than relying on occasional manual reviews.
Impersonation and module execution context: why EXECUTE AS can defeat your assumptions
Impersonation is a legitimate SQL Server feature. It allows a module to run under another database or server principal so users can perform a controlled task without receiving broad direct permissions.
The danger appears when impersonation is used as a blanket elevation mechanism, especially through EXECUTE AS OWNER or EXECUTE AS dbo, or when dynamic SQL is executed inside an impersonated module.
DBAs sometimes assume an explicit DENY is a reliable backstop. That assumption can fail if the principal can later run the operation under a different execution context.
For example, a user who can become dbo, directly or indirectly, can perform actions as dbo even when those actions would be denied to the original user. This is one reason db_owner membership is so sensitive: it changes what the user can become, not only what the user can do directly.
What are the warning signs a DBA should look for?
- Modules created with
EXECUTE AS OWNER,EXECUTE AS dbo, or an unusually privileged user.
- Dynamic SQL inside impersonated stored procedures, functions, or triggers.
- Explicit
IMPERSONATEpermissions granted to application, vendor, support, or developer accounts.
- Databases with
TRUSTWORTHYON, and modules that execute as owner or dbo.
- Users manually running
EXECUTE AS USERorEXECUTE AS LOGINoutside of approved administrative workflows.
What to monitor, and how
| Area | What to check | How to monitor |
| Module context | Stored procedures, triggers, functions, and views with execute_as_principal_id set. | Query sys.sql_modules regularly in each database. Review every module using EXECUTE AS that is not part of an approved pattern. |
| Impersonation grants | Database and server IMPERSONATE permissions, especially on dbo, sa, privileged logins, or service accounts. | Query sys.database_permissionsand sys.server_permissions. Alert when IMPERSONATE is granted or denied outside approved changes. |
| Runtime use | EXECUTE AS USEREXECUTE AS LOGINREVERTOther modules running under unexpected principals. | Extended Events:audit_database_principal_impersonation_event and audit_server_principal_impersonation_event. SQL Audit for permission and principal changes. |
| Dynamic SQL under elevation | EXECsp_executesqlOPENQUERYlinked server execution xp_cmdshellOLE Automation external scripts | Static code review plus XE sql_statement_completed and module_end for high-risk modules. |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/* Modules that execute under a different context */ SELECT DB_NAME() AS database_name, SCHEMA_NAME(o.schema_id) AS schema_name, o.name AS object_name, o.type_desc, m.execute_as_principal_id, USER_NAME(m.execute_as_principal_id) AS execute_as_principal_name, o.create_date, o.modify_date FROM sys.sql_modules AS m JOIN sys.objects AS o ON m.object_id = o.object_id WHERE m.execute_as_principal_id IS NOT NULL ORDER BY o.modify_date DESC; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* Server-level impersonation grants */ SELECT grantor.name AS grantor_name, grantee.name AS grantee_name, sp.state_desc, sp.permission_name, sp.class_desc, target_principal.name AS target_principal FROM sys.server_permissions AS sp LEFT JOIN sys.server_principals AS grantor ON sp.grantor_principal_id = grantor.principal_id LEFT JOIN sys.server_principals AS grantee ON sp.grantee_principal_id = grantee.principal_id LEFT JOIN sys.server_principals AS target_principal ON sp.major_id = target_principal.principal_id WHERE sp.permission_name = N'IMPERSONATE' ORDER BY grantee.name, target_principal.name; |
Recommended fixes and mitigations
- Avoid
EXECUTE AS dboandEXECUTE AS OWNERas a default pattern. Use them only when the trust boundary is fully understood.
- Prefer certificate-signed modules for controlled privilege elevation. Module signing can grant a specific permission to a specific module without broadly changing the caller’s execution context.
- Remove unnecessary
IMPERSONATEgrants. TreatIMPERSONATEon privileged principals as equivalent to a high-privilege administrative grant.
- Do not allow unparameterized dynamic SQL inside impersonated modules. Parameterize, validate identifiers with
QUOTENAMEwhere appropriate, and avoid concatenating user-controlled input.
- Review modules again after restores, vendor deployments, and application upgrades. These are common moments for elevated execution context to appear unexpectedly.
msdb and SQL Server Agent exposure: why msdb is a server-level security boundary
msdb is not just a place where job history is stored. It contains SQL Server Agent jobs, job steps, schedules, operators, alerts, credentials, proxies, backup and restore history, Database Mail metadata, maintenance plans, and SSIS-related metadata. A user with excessive permissions in msdb may be able to influence how privileged automation runs on the instance.
SQL Server Agent is especially important because a job step may run as the SQL Agent service account, as the job owner, or through a proxy credential depending on the subsystem and configuration.
A job that looks like routine maintenance can become a privilege boundary if a lower-privileged user can modify the job, modify objects that the job touches, or control parameters used by the job.
What are the warning signs a DBA should look for?
- Non-DBA users in
SQLAgentUserRole,SQLAgentReaderRole, orSQLAgentOperatorRole.
- Jobs owned by sa or sysadmin logins where the job touches databases controlled by non-sysadmin
db_ownerusers.
- Job steps that execute dynamic T-SQL, PowerShell, CmdExec, SSIS packages, replication agents, or external scripts.
- Proxies and credentials that are unused, shared across unrelated jobs, or more privileged than needed.
- Jobs created or modified by unexpected users, especially outside deployment windows.
- Maintenance jobs that run against all databases without accounting for untrusted database owners or triggers.
What to monitor and how
| Area | What to check | How to monitor |
| Agent roles | Membership in:msdb SQLAgentUserRoleSQLAgentReaderRoleSQLAgentOperatorRole | Query msdb.sys.database_role_members. Alert when non-DBA accounts are added. |
| Job ownership | Jobs owned by sa, sysadmin logins, disabled logins, vendor accounts, or generic accounts. | Query msdb.dbo.sysjobs and sys.server_principals. Baseline expected owners. |
| Job changes | Job creation, deletion, step changes, schedule changes, proxy changes. | SQL Audit object/permission changes in msdb. Extended Events rpc_completed andsql_batch_completed filtered on: sp_add_jobsp_update_jobsp_add_jobstepsp_update_jobstep, sp_delete_jobsp_start_job |
| Job execution | Unexpected manual starts, failures, reruns, long-running steps, commands changed before execution. | msdb.dbo.sysjobhistory, sysjobactivity, SQL Agent logs, and Windows event logs. Alert on failure patterns and changes near execution time. |
| Credentials/proxies | Proxy mappings to subsystems, credential identities, jobs using high-privilege proxies. | Query msdb proxy and credential metadata. Review with Windows/local admin team for actual OS rights. |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* SQL Agent fixed role membership */ USE msdb; GO SELECT role_principal.name AS role_name, member_principal.name AS member_name, member_principal.type_desc AS member_type FROM sys.database_role_members AS drm JOIN sys.database_principals AS role_principal ON drm.role_principal_id = role_principal.principal_id JOIN sys.database_principals AS member_principal ON drm.member_principal_id = member_principal.principal_id WHERE role_principal.name IN ( N'SQLAgentUserRole', N'SQLAgentReaderRole', N'SQLAgentOperatorRole' ) ORDER BY role_principal.name, member_principal.name; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/* SQL Agent jobs, owners, and high-risk subsystems */ SELECT j.name AS job_name, SUSER_SNAME(j.owner_sid) AS job_owner, CASE WHEN IS_SRVROLEMEMBER('sysadmin', SUSER_SNAME(j.owner_sid)) = 1 THEN 'sysadmin owner' ELSE 'non-sysadmin owner' END AS owner_risk_note, s.step_id, s.step_name, s.subsystem, LEFT(s.command, 4000) AS command_text FROM msdb.dbo.sysjobs AS j LEFT JOIN msdb.dbo.sysjobsteps AS s ON j.job_id = s.job_id WHERE s.subsystem IN (N'TSQL', N'PowerShell', N'CmdExec', N'SSIS', N'ActiveScripting') OR IS_SRVROLEMEMBER('sysadmin', SUSER_SNAME(j.owner_sid)) = 1 ORDER BY j.name, s.step_id; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/* Recent job changes and execution activity */ SELECT TOP (200) j.name AS job_name, a.start_execution_date, a.stop_execution_date, a.run_requested_date, a.run_requested_source, a.last_executed_step_id, j.date_created, j.date_modified, SUSER_SNAME(j.owner_sid) AS job_owner FROM msdb.dbo.sysjobs AS j LEFT JOIN msdb.dbo.sysjobactivity AS a ON j.job_id = a.job_id ORDER BY COALESCE(a.start_execution_date, j.date_modified, j.date_created) DESC; |
Recommended fixes and mitigations
- Restrict SQL Agent role membership. Do not grant
SQLAgentOperatorRoleorSQLAgentReaderRolebroadly just because a user needs to see job status.
- Use dedicated job owner accounts. Avoid using personal DBA accounts as job owners. Avoid
saas the default owner unless there is a clear reason and compensating monitoring.
- Separate job administration from database ownership. A user who controls a database should not automatically be able to influence a sysadmin-owned job that touches that database.
- Review job steps for dynamic SQL and external execution. CmdExec, PowerShell, SSIS, and external scripts deserve special attention because they can cross the database/host boundary.
- Remove unused proxies and credentials. For required proxies, use least-privilege Windows accounts and limit each one to only the necessary subsystems.
- Baseline job definitions and alert when a job command, owner, proxy, subsystem, or schedule changes.
Privileged maintenance jobs and trigger hijacking: why maintenance can become an escalation path
This is the most subtle part of the chain. DML (data manipulation language) and DDL (data definition or declaration language) triggers don’t execute under the permissions of the trigger creator. Instead, they execute when another statement fires them, and the effective context is tied to the caller and the execution path that fired the trigger. If a highly privileged maintenance job performs an operation that fires a trigger in a database controlled by a lower-privileged db_owner, the trigger may be able to hijack the permissions of that privileged operation.
This matters for consolidated SQL Server instances where different teams or applications own different databases, but central DBA jobs run maintenance across all databases. The local db_owner may not be sysadmin. The maintenance job may be sysadmin. The trigger is the bridge between them.
The risk is not limited to intentionally malicious users. It also affects untrusted restores, vendor databases, old applications, databases migrated from other servers, and environments where developers or third parties have broad database rights.
Important caveat: BACKUP DATABASE and DBCC CHECKDB don’t fire the same DDL triggers as operations such as ALTER INDEX or UPDATE STATISTICS. The exposure depends on what the maintenance job actually does. Inventory the job steps before deciding which databases are safe to touch with privileged context.
What are the warning signs a DBA should look for?
- DDL triggers at database scope, especially triggers listening to broad event groups such as
DDL_DATABASE_LEVEL_EVENTS.
- DML triggers on tables touched by cleanup, archiving, ETL (extract, transform, load), reporting refresh, replication, change data capture (CDC), or maintenance processes.
- Triggers modified shortly before a privileged job runs.
- Triggers containing
ALTER SERVER ROLE,CREATE LOGIN,GRANT,IMPERSONATE,xp_cmdshell, OLE Automation, linked server calls, external script execution, or dynamic SQL.
- Maintenance jobs running as
saor sysadmin across databases owned by application teams, vendors, or restored from external sources.
- Databases with
TRUSTWORTHY ONand sysadmin ownership. This can defeat the protective sandbox thatEXECUTE AS USERwould otherwise provide.
What to monitor and how
| Area | What to check | How to monitor |
| Database triggers | DML and DDL triggers, disabled status, create/modify dates, parent object, event group coverage. | In each database, query the following:sys.triggerssys.trigger_eventssys.sql_modulesAlso, alert on CREATE/ALTER/DROP TRIGGER. |
| Server triggers | Server-level DDL triggers and logon triggers. | Query the following:sys.server_triggerssys.server_trigger_eventssys.server_sql_modulesAlso, audit SERVER_OBJECT_CHANGE_GROUP. |
| Suspicious trigger content | Dynamic SQL, server-role changes, login creation, GRANT/ALTER AUTHORIZATION, xp_cmdshell, sp_OA*, external scripts, linked-server execution. | Static code scanning of trigger definitions. XE/SQL Audit for module execution and privileged statements. |
| Maintenance execution context | Which login owns the job, whether the step runs as a proxy, whether the job uses EXECUTE AS, and which databases are touched. | Review msdb job metadata and job history. Capture job start/stop and command text changes. Baseline all maintenance jobs. |
| Privileged operations fired inside user DBs | ALTER INDEX, UPDATE STATISTICS, schema changes, data cleanup, ETL changes, replication/CDC maintenance. | XE sql_statement_completedobject_alteredobject_createdobject_deletedSQL Audit DATABASE_OBJECT_CHANGE_GROUP and SCHEMA_OBJECT_CHANGE_GROUP. |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* Database triggers in the current database */ SELECT DB_NAME() AS database_name, SCHEMA_NAME(o.schema_id) AS parent_schema, OBJECT_NAME(t.parent_id) AS parent_object, t.name AS trigger_name, t.parent_class_desc, t.is_disabled, t.create_date, t.modify_date, LEFT(m.definition, 4000) AS trigger_definition_sample FROM sys.triggers AS t LEFT JOIN sys.objects AS o ON t.parent_id = o.object_id LEFT JOIN sys.sql_modules AS m ON t.object_id = m.object_id ORDER BY t.modify_date DESC; |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
/* Server-level triggers */ SELECT st.name AS trigger_name, st.parent_class_desc, st.is_disabled, st.create_date, st.modify_date, LEFT(sm.definition, 4000) AS trigger_definition_sample FROM sys.server_triggers AS st LEFT JOIN sys.server_sql_modules AS sm ON st.object_id = sm.object_id ORDER BY st.modify_date DESC; |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
/* Trigger event coverage */ SELECT DB_NAME() AS database_name, t.name AS trigger_name, te.type_desc AS trigger_event, t.parent_class_desc, t.is_disabled, t.modify_date FROM sys.triggers AS t LEFT JOIN sys.trigger_events AS te ON t.object_id = te.object_id ORDER BY t.name, te.type_desc; |
A safer maintenance pattern
When a central maintenance job must run DDL or DML inside databases that may be controlled by non-sysadmin owners, avoid letting the job’s server-level privileges be hijacked by code inside those databases. One defensive pattern is to execute the database-level maintenance under a database user context, commonly dbo, so the operation remains inside the database boundary instead of carrying the sysadmin token into trigger execution.
|
1 2 3 4 5 6 7 8 9 |
/* Pattern for a carefully reviewed maintenance step */ USE [ApplicationDatabase]; GO EXECUTE AS USER = 'dbo'; GO ALTER INDEX ALL ON dbo.SomeTable REBUILD; GO REVERT; GO |
This pattern is useful because a database-level EXECUTE AS context does not automatically carry server-level privileges. However, it’s not magic. If the database is TRUSTWORTHY ON and owned by a sysadmin, or if unsafe cross-database ownership chains exist, the protection may be weakened or bypassed. Always review database owner, TRUSTWORTHY, DB_CHAINING, and module execution context before relying on this pattern.
If you use Ola Hallengren’s Maintenance Solution, review the @ExecuteAsUser option for maintenance commands that run inside user databases. This can help reduce the risk of server-level permission hijacking during index and statistics maintenance, provided the database trust settings are also safe.
Recommended fixes and mitigations
- Don’t run all maintenance across all databases as an unrestricted sysadmin context without considering database trust boundaries.
- Inventory triggers before onboarding a database into central maintenance, especially after restore, migration, vendor deployment, or application upgrade.
- Wrap database-local DDL/DML maintenance in a database-level
EXECUTE AScontext where appropriate and tested.
- Use dedicated maintenance accounts and job owners. Avoid personal accounts. Avoid using
saas a convenience default for every job.
- Disable or remove unexpected triggers. Require change approval for any trigger that can fire during privileged maintenance.
- Keep
TRUSTWORTHY OFFand avoid sysadmin-owned user databases unless explicitly required and documented.
- Consider running maintenance for untrusted or vendor databases in separate jobs with additional logging and a more restrictive execution context.
A practical monitoring implementation for SQL Server – what you should do
A good SQL Server monitoring design combines configuration checks, runtime telemetry, and change auditing. None of these controls is enough on its own, though. You’ll also benefit from catalog queries, which tell you what exists now, and SQL Audit, covering who changed permissions and objects.
There’s also Extended Events, which helps capture runtime behavior, and SQL Agent history explains when automation actually ran.
You may also be interested in…
Recommended SQL Audit coverage
The exact audit configuration depends on edition, compliance requirements, storage location, and performance tolerance, but the following action groups are a practical starting point for this topic:
DATABASE_ROLE_MEMBER_CHANGE_GROUP– changes to database role membership, includingdb_ownerand SQL Agent fixed database roles in msdb.
SERVER_ROLE_MEMBER_CHANGE_GROUP– changes to sysadmin and other fixed server roles.
DATABASE_PERMISSION_CHANGE_GROUPandSERVER_PERMISSION_CHANGE_GROUP– permission grants, denies, and revokes.
DATABASE_PRINCIPAL_CHANGE_GROUPandSERVER_PRINCIPAL_CHANGE_GROUP– user and login creation, alteration, and deletion.
DATABASE_OBJECT_CHANGE_GROUPandSCHEMA_OBJECT_CHANGE_GROUP– creation, alteration, or deletion of procedures, triggers, schemas, and other database objects.
SERVER_OBJECT_CHANGE_GROUP– changes to server-level objects, including server-level triggers.
SCHEMA_OBJECT_ACCESS_GROUP– optional, higher-volume auditing for execution/access to specific high-risk procedures or schemas.
Recommended Extended Events coverage
Use Extended Events for targeted telemetry. Avoid capturing every statement on every production server without filters. Start with a small, high-signal session and expand only where needed.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
/* Example XE session for high-risk administrative activity. Adjust filters and targets for your environment before production use. */ CREATE EVENT SESSION [DBA_Security_Exposure_Watch] ON SERVER ADD EVENT sqlserver.object_created, ADD EVENT sqlserver.object_altered, ADD EVENT sqlserver.object_deleted, ADD EVENT sqlserver.rpc_completed ( ACTION ( sqlserver.client_app_name, sqlserver.client_hostname, sqlserver.database_name, sqlserver.server_principal_name, sqlserver.session_id, sqlserver.sql_text ) WHERE ( sqlserver.database_name = N'msdb' OR sqlserver.like_i_sql_unicode_string(sqlserver.sql_text, N'%sp_add_job%') OR sqlserver.like_i_sql_unicode_string(sqlserver.sql_text, N'%sp_update_job%') OR sqlserver.like_i_sql_unicode_string(sqlserver.sql_text, N'%CREATE TRIGGER%') OR sqlserver.like_i_sql_unicode_string(sqlserver.sql_text, N'%ALTER TRIGGER%') OR sqlserver.like_i_sql_unicode_string(sqlserver.sql_text, N'%EXECUTE AS%') ) ) ADD TARGET package0.event_file ( SET filename = N'C:\Temp\DBA_Security_Exposure_Watch.xel', max_file_size = 100, max_rollover_files = 10 ); GO ALTER EVENT SESSION [DBA_Security_Exposure_Watch] ON SERVER STATE = START; GO |
Operational checklist for monitoring SQL Server
- Daily: review changes to sysadmin membership,
db_ownermembership, SQL Agent roles, job owners, job steps, and trigger definitions.
- Weekly: compare permissions, database settings, module execution contexts, and SQL Agent proxies against the approved baseline.
- Before maintenance onboarding: inspect the target database for triggers,
TRUSTWORTHY,DB_CHAINING, owner, CLR assemblies, Service Broker activation, andEXECUTE ASmodules.
- After restore or vendor deployment: treat the database as untrusted until programmable objects, owners, permissions, triggers, and jobs are reviewed.
- After incident or suspicious change: preserve SQL Audit files, XE files, SQL Agent logs, default trace data if still available, Windows event logs, and msdb job history before cleanup.
Future-proof database monitoring with Redgate Monitor
A prioritized SQL Server hardening plan
If you need to turn this guidance into an action plan, start with the changes that reduce the largest number of attack paths without requiring application redesign.
- Inventory and remove permanent
db_ownerfrom application, support, vendor, and reporting accounts.
- Review
msdbrole membership and SQL Agent job ownership. Move jobs to dedicated owner accounts and remove unnecessary operators.
- Baseline SQL Agent jobs and alert on job command, owner, proxy, subsystem, and schedule changes.
- Inventory DDL and DML triggers across all user databases and server scope. Investigate recently modified triggers first.
- Review all modules using
EXECUTE AS OWNER,EXECUTE AS dbo, or privileged execution context.
- Keep
TRUSTWORTHY OFFand avoid sysadmin-owned user databases unless there is a documented exception.
- Implement SQL Audit for role, permission, principal, and object changes. Add targeted XE sessions for impersonation, trigger changes, and SQL Agent procedure calls.
- Change maintenance jobs so database-local operations run in a safer database-level context where appropriate and tested.
- Create a restore intake process for untrusted or vendor databases before they’re introduced to production maintenance jobs.
Conclusion
SQL Server security is not only about disabling obvious risky features. Instead, it’s more about understanding where trust crosses boundaries.
A db_owner user, for example, is not merely a user who can write data. msdb is not merely a history database. SQL Agent is not merely a scheduler. A trigger is not merely application logic. When these features interact, routine administration can become an attack path.
DBAs are in the best position to reduce this risk because the exposure is usually visible in the metadata: role membership, job ownership, proxy usage, trigger definitions, module execution context, database ownership, and trust settings. As such, finding the data is easy. Treating the data as a security boundary and continuously monitoring it, however, is more difficult.
Moving from secure by default to just secure means verifying every privilege that can change code, every job that runs with elevated rights, and every database that central automation touches. Once those trust relationships are visible, they can be reduced, monitored, and controlled.
Simple Talk is brought to you by Redgate Software
FAQs: How to stop SQL Server maintenance becoming an attack path
1. Is SQL Server secure by default?
Not entirely. Default installations and default permissions still leave several attack surfaces open, including broad public role grants, legacy compatibility features, and administrative conveniences. “Secure by default” doesn’t mean a DBA can skip reviewing role membership, permissions, and trust settings.
2. What is trigger hijacking in SQL Server?
Trigger hijacking happens when a DML or DDL trigger created by a lower-privileged user fires during an operation run by a highly privileged process, such as a sysadmin-owned maintenance job. Because the trigger’s effective context is tied to the caller that fired it, the trigger can inherit and misuse that caller’s elevated permissions.
3. Why is db_owner a security risk for application accounts?
A db_owner member can create and alter objects, define triggers, change permissions, and build modules with elevated execution context, effectively controlling the database’s security boundary. This is far more access than most runtime application or support accounts need, and it becomes especially dangerous when combined with privileged maintenance jobs or impersonation.
4. Why does msdb matter for SQL Server security?
msdb stores SQL Server Agent jobs, schedules, credentials, proxies, and job history, and job steps can run under the SQL Agent service account, the job owner, or a proxy. A user with excessive permissions in msdb can potentially influence how privileged automation executes on the instance, making it a server-level security boundary rather than just a metadata store.
5. Can EXECUTE AS OWNER or EXECUTE AS dbo bypass an explicit DENY?
Yes, in some cases. If a principal can run code under a different execution context — such as becoming dbo through an impersonated module — actions that would normally be denied to that user can still execute under the impersonated identity. This is one reason db_owner membership and broad IMPERSONATE grants are treated as high-risk.
6. How can a DBA check whether their SQL Server instance is exposed to this kind of risk?
Start by querying system catalog views such as sys.database_role_members, sys.database_permissions, sys.sql_modules, and sys.triggers to baseline role membership, permissions, execution context, and trigger definitions. Pair this with SQL Audit (role, permission, and object change groups) and targeted Extended Events sessions to catch drift and suspicious activity over time.
This document contains proprietary information and is protected by copyright law.
Copyright © 2026 Red Gate Software Limited. All rights reserved
Load comments