{"id":112093,"date":"2026-10-09T12:00:00","date_gmt":"2026-10-09T12:00:00","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=112093"},"modified":"2026-09-29T15:10:50","modified_gmt":"2026-09-29T15:10:50","slug":"how-to-secure-temporary-objects-metadata-visibility-and-audit-assurance-in-sql-server-complete-guide","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/how-to-secure-temporary-objects-metadata-visibility-and-audit-assurance-in-sql-server-complete-guide\/","title":{"rendered":"How to secure temporary objects, metadata visibility, and audit assurance\u00a0in SQL Server (complete guide)"},"content":{"rendered":"\n<p><strong>SQL Server environments that are patched, firewalled, and configured to best practice can still carry hidden risk. Global temporary objects, Dynamic Management Views (DMVs), and audit specifications are all legitimate, widely-used SQL Server capabilities. Yet, each can quietly become part of an attack chain when granted too broadly, trusted inside privileged workflows, or <em>only<\/em> validated against the obvious case.<\/strong> <\/p>\n\n\n\n<p><strong>This article, part of Fabiano Amorim&#8217;s <a href=\"https:\/\/www.red-gate.com\/simple-talk\/collections\/fabiano-amorims-complete-guide-to-sql-server-security\/\" target=\"_blank\" rel=\"noreferrer noopener\">complete guide to SQL Server security<\/a>, explains and demonstrates where these three features commonly go wrong. Learn the the exact signs and queries to check for exposure &#8211; and the practical fixes that turn a theoretical risk into a closed gap.<\/strong><\/p>\n\n\n\n<p>Over the course of <a href=\"https:\/\/www.red-gate.com\/simple-talk\/collections\/fabiano-amorims-complete-guide-to-sql-server-security\/\" target=\"_blank\" rel=\"noreferrer noopener\">this series<\/a>, I&#8217;ve challenged the common assumption that a <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">SQL Server<\/a> environment is safe simply because it&#8217;s patched, firewalled, and configured according to familiar defaults.<\/p>\n\n\n\n<p>I&#8217;ve proven that this is not quite true, because <em>real<\/em> security is not a checkbox. It requires a verification mindset, especially in areas where SQL Server intentionally provides powerful operational features for DBAs, developers, <a href=\"https:\/\/www.red-gate.com\/products\/redgate-monitor\/\" target=\"_blank\" rel=\"noreferrer noopener\">monitoring tools<\/a>, and application frameworks.&nbsp;<\/p>\n\n\n\n<p>In this guide, I&#8217;ll focus on three features that are easy to underestimate because they look like normal SQL Server behavior. They are global temporary objects, <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/system-dynamic-management-objects\/system-dynamic-management-objects?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">Dynamic Management Views (DMVs)<\/a>, and audit coverage &#8211; each a useful feature in their own right. <\/p>\n\n\n\n<p>The other thing they all have in common? They can each become part of an attack chain if granted too broadly, used inside privileged workflows, or trusted without validation.&nbsp;<\/p>\n\n\n\n<p><strong>My goal here is not to provide a deep exploit walkthrough &#8211; that&#8217;s for my <a href=\"https:\/\/www.red-gate.com\/simple-talk\/collections\/sql-server-security-vulnerabilities-you-werent-aware-of\/\" target=\"_blank\" rel=\"noreferrer noopener\">other series<\/a>. Here, I want to help you, a DBA, answer the practical questions. Where am I exposed? What should I look for (and what should I&nbsp;monitor?) Then, what should I change when I find a risky pattern?&nbsp;<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-why-nbsp-these-sql-server-risks-matter\">Why&nbsp;these SQL Server risks matter<\/h2>\n\n\n\n<p>The risks in this article don&#8217;t necessarily start with&nbsp;an obviously&nbsp;dangerous permission. They may, in fact, look quite reasonable in isolation. <\/p>\n\n\n\n<p>You might not bat an eyelid at, for example, a developer using a 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 table<\/a>, a vendor requesting <code>VIEW SERVER STATE<\/code>, or an auditor enabling a&nbsp;sensitive-data&nbsp;audit group. The danger comes when these choices are combined with elevated <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 Agent<\/a> jobs, predictable object names, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/oracle-databases\/writing-better-dynamic-sql\/\" target=\"_blank\" rel=\"noreferrer noopener\">dynamic SQL<\/a>, broad <a href=\"https:\/\/www.red-gate.com\/products\/redgate-monitor\/\" target=\"_blank\" rel=\"noreferrer noopener\">monitoring visibility<\/a>, or narrow audit assumptions.&nbsp;<\/p>\n\n\n\n<p>Often times, an attacker with a low-privileged foothold only needs to influence something that a trusted process will later consume,&nbsp;observe&nbsp;enough metadata to understand where the trust boundaries are, or choose an access path that was never included in the <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/sql-server-audit-server-configuration\/\" target=\"_blank\" rel=\"noreferrer noopener\">audit test plan<\/a>. <\/p>\n\n\n\n<p><strong>All of this is exactly why SQL Server DBAs should take the security of these features seriously, and treat them as operational trust boundaries. In this guide, I&#8217;ll help you to achieve this.<\/strong><\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-sql-server-global-temporary-tables-nbsp-and-nbsp-procedures-nbsp\">SQL Server global temporary tables&nbsp;and&nbsp;procedures&nbsp;<\/h2>\n\n\n\n<p><strong>SQL Server temporary objects are stored in\u00a0<code><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/databases\/tempdb-database?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">tempdb<\/a><\/code>. Local temporary tables, prefixed with a single #, are scoped to the creating session. Global <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/theory-and-design\/temporary-tables\/\" target=\"_blank\" rel=\"noreferrer noopener\">temporary tables<\/a> and global <a href=\"https:\/\/www.red-gate.com\/simple-talk\/other\/using-temporary-procedures\/\" target=\"_blank\" rel=\"noreferrer noopener\">temporary procedures<\/a> prefixed with ##, meanwhile, are different<\/strong> <strong>&#8211; they&#8217;re visible\u00a0outside the session that created them. <\/strong><\/p>\n\n\n\n<p><strong>This distinction is where many design mistakes begin, as temporary does <em>not<\/em> mean\u00a0&#8216;private&#8217; &#8211; and global means &#8216;shared&#8217;.\u00a0<\/strong><\/p>\n\n\n\n<p>The risk becomes serious when a privileged process relies on a predictable global temporary object. Examples include SQL Server Agent jobs <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\">running as&nbsp;sa<\/a>, maintenance procedures executing as&nbsp;<a href=\"https:\/\/www.red-gate.com\/simple-talk\/other\/securing-access-administrator\/\" target=\"_blank\" rel=\"noreferrer noopener\">dbo<\/a>, vendor routines that use global scratch tables, or administrative scripts that pass state between job steps through ## objects. <\/p>\n\n\n\n<p>If lower-privileged users can predict the name,\u00a0observe\u00a0the lifecycle, pre-create a competing object, or influence the rows consumed by the privileged process, the temporary object becomes a trust-boundary problem.\u00a0It&#8217;s that simple.<\/p>\n\n\n\n<p>Global temporary procedures deserve special attention. A global temporary table may allow data poisoning, and a global temporary procedure is executable code. If privileged code executes a ## procedure by name, the important security question is: <em>who controlled the code body that name&nbsp;resolved to&nbsp;at runtime?<\/em><\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-an-example-of-this-in-action\">An example of this in action<\/h3>\n\n\n\n<p>Consider a custom SQL Agent job that performs nightly user provisioning. It first creates a global table called <code>##UserProvisioningQueue<\/code> and loads rows from an external feed. Then, it reads that table and builds dynamic SQL to create logins or assign roles. The job owner is\u00a0<code>sa<\/code>\u00a0because the workflow performs server-level administration.\u00a0<\/p>\n\n\n\n<p>A compromised low-privileged account that can discover this pattern may try to create the same global object <em>before<\/em> the job&nbsp;starts &#8211; or&nbsp;may wait for the object to appear and attempt to insert crafted rows. <\/p>\n\n\n\n<p>If the privileged job doesn&#8217;t&nbsp;validate&nbsp;the&nbsp;object&nbsp;origin, schema, and content, it may process attacker-controlled input under the&nbsp;<code>sa<\/code>&nbsp;context.<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">\/* Find SQL Agent job steps that reference global temporary objects *\/\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.database_name,\n    s.command\nFROM msdb.dbo.sysjobs AS j\nJOIN msdb.dbo.sysjobsteps AS s\n    ON j.job_id = s.job_id\nWHERE s.command LIKE '%##%'\nORDER BY j.name, s.step_id;\n\n\/* Find programmable objects that reference ## objects *\/\nSELECT\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\nFROM sys.sql_modules AS m\nJOIN sys.objects AS o\n    ON m.object_id = o.object_id\nWHERE m.definition LIKE '%##%'\nORDER BY schema_name, object_name;<\/pre><\/div>\n\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>SQL Agent job steps, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/other\/for-the-love-of-stored-procedures\/\" target=\"_blank\" rel=\"noreferrer noopener\">stored procedures<\/a>, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/transforming-and-analyzing-data-in-postgresql\/#:~:text=%E2%80%9CExtract%20Transform%20Load%E2%80%9D%20(ETL)\" target=\"_blank\" rel=\"noreferrer noopener\">ETL<\/a> routines, or vendor scripts that create or consume objects with names beginning with ##.<br><br><\/li>\n\n\n\n<li>Privileged jobs that pass security-sensitive state between job steps through global temporary tables instead of local temp tables or permanent secured staging tables.<br><br><\/li>\n\n\n\n<li>Global temporary procedures in <code>tempdb<\/code> &#8211; especially if their names are predictable or if they&#8217;re executed by elevated jobs.<br><br><\/li>\n\n\n\n<li>Dynamic SQL built from rows stored in temporary tables, particularly when the job owner is <code>sa<\/code> or another sysadmin-equivalent principal.<br><br><\/li>\n\n\n\n<li>Repeated errors such as <em>&#8216;There is already an object named&#8230;&#8217;<\/em> around the same time privileged jobs start, which may indicate name collisions or probing.<\/li>\n<\/ul>\n<\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"h-what-to-monitor-and-how\">What to monitor &#8211; and how<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-code-and-job-step-review\">Code and job-step review<\/h4>\n\n\n\n<p id=\"h-code-and-job-step-reviewschedule-a-recurring-check-against-msdb-dbo-sysjobsteps-and-sys-sql-modules-for-references-to-treat-any-result-in-a-privileged-workflow-as-something-that-needs-design-review-not-just-code-cleanup\">Schedule a recurring check against <code>msdb.dbo.sysjobsteps<\/code> and <code>sys.sql_modules<\/code> for references to ##. Treat any result in a privileged workflow as something that needs design review, not just code cleanup.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-current-tempdb-object-inventory\">Current <code>tempdb<\/code> object inventory<\/h4>\n\n\n\n<p id=\"h-current-tempdb-object-inventoryperiodically-sample-tempdb-for-global-temporary-objects-and-correlate-creation-windows-with-privileged-jobs-this-is-especially-useful-during-testing-and-incident-response\">Periodically sample <code>tempdb<\/code> for global temporary objects and correlate creation windows with <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\">privileged jobs<\/a>. This is especially useful during testing and incident response.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-extended-events\">Extended Events<\/h4>\n\n\n\n<p id=\"h-extended-eventscapture-object-creation-and-ddl-activity-in-tempdb-together-with-session-id-username-client-app-name-client-hostname-and-sql-text-validate-the-exact-event-fields-in-your-sql-server-version-before-promoting-the-session-to-production\">Capture object creation and <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/theory-and-design\/data-control-language-aka-security\/\" target=\"_blank\" rel=\"noreferrer noopener\">DDL<\/a> (data declaration language) activity in <code>tempdb<\/code>, together with <code>session_id<\/code>, <code>username<\/code>, <code>client_app_name<\/code>, <code>client_hostname<\/code>, and <code>sql_text<\/code>. Validate the exact event fields in your SQL Server version before promoting the session to production.<\/p>\n\n\n\n<section id=\"my-first-block-block_0c23e599ac99f2e620d6120ac5be1fae\" 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<h4 class=\"wp-block-heading\" id=\"h-sql-audit\">SQL Audit<\/h4>\n\n\n\n<p id=\"h-sql-auditwhere-appropriate-layer-schema-object-change-auditing-with-job-definition-monitoring-audit-alone-is-not-a-substitute-for-removing-the-risky-design-but-it-helps-detect-unexpected-object-creation-and-modification\">Where appropriate, layer schema\/object change auditing with job definition monitoring. Audit alone is not a substitute for removing the risky design, but it helps detect unexpected object creation and modification.<\/p>\n\n\n\n<h4 class=\"wp-block-heading\" id=\"h-job-history\">Job history<\/h4>\n\n\n\n<p id=\"h-job-historyeview-job-failures-and-retries-around-steps-that-create-global-temporary-objects-a-sudden-collision-or-object-exists-error-can-be-a-signal-that-another-session-created-the-expected-object-first\">Review job failures and retries around steps that create global temporary objects. A sudden collision or object-exists error can be a signal that another session created the expected object first.<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">\/* Current global temporary objects in tempdb *\/\nSELECT\n    o.name,\n    o.type_desc,\n    o.create_date,\n    o.modify_date\nFROM tempdb.sys.objects AS o\nWHERE o.name LIKE '##%'\nORDER BY o.create_date DESC;\n\n\/* Job failures near global-temp workflows *\/\nSELECT TOP (100)\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.message\nFROM msdb.dbo.sysjobhistory AS h\nJOIN msdb.dbo.sysjobs AS j\n    ON h.job_id = j.job_id\nWHERE h.run_status &lt;&gt; 1\n  AND EXISTS\n  (\n      SELECT 1\n      FROM msdb.dbo.sysjobsteps AS s\n      WHERE s.job_id = j.job_id\n        AND s.command LIKE '%##%'\n  )\nORDER BY h.instance_id DESC;\n\/* XE starting point: detect batches\/RPCs that reference global temp objects *\/\nCREATE EVENT SESSION [Watch_Global_Temp_Object_References]\nON SERVER\nADD EVENT sqlserver.sql_batch_completed\n(\n    ACTION\n    (\n        sqlserver.session_id,\n        sqlserver.server_principal_name,\n        sqlserver.database_name,\n        sqlserver.client_hostname,\n        sqlserver.client_app_name\n    )\n    WHERE (sqlserver.like_i_sql_unicode_string([batch_text], N'%##%'))\n),\nADD EVENT sqlserver.rpc_completed\n(\n    ACTION\n    (\n        sqlserver.session_id,\n        sqlserver.server_principal_name,\n        sqlserver.database_name,\n        sqlserver.client_hostname,\n        sqlserver.client_app_name\n    )\n    WHERE (sqlserver.like_i_sql_unicode_string([statement], N'%##%'))\n)\nADD TARGET package0.event_file\n(\n    SET filename = N'C:\\SQLAudit\\Watch_Global_Temp_Object_References.xel',\n        max_file_size = 100,\n        max_rollover_files = 5\n);\nGO\nALTER EVENT SESSION [Watch_Global_Temp_Object_References] ON SERVER STATE = START;\nGO<\/pre><\/div>\n\n\n\n<div id=\"callout-block_d0edb91a2bf0b676f4d307da0a80d1c4\" 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>Practical DBA interpretation<\/strong> <\/p>\n\n\n\n<p>Finding ## in code is not automatically a vulnerability. The risk depends on who creates it, who can see it (and write to it), the predictability of its name, whether privileged code trusts it, and whether it feeds dynamic SQL or administrative decisions. The review should focus on the trust boundary rather than just the prefix.<\/p>\n\n<\/div>\n<\/div> \n\n\n<h3 class=\"wp-block-heading\">Recommended fixes or mitigations<\/h3>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><strong>Remove global temporary objects from privileged workflows, <\/strong>using local temporary tables where possible. Local # tables are session-scoped and avoid the cross-session trust problem.<br><br><\/li>\n\n\n\n<li><strong>Use secured permanent staging tables when state must cross sessions. <\/strong>If multiple job steps or sessions <em>must<\/em> exchange data, use a <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/learn\/schema-based-access-control-for-sql-server-databases\/\" target=\"_blank\" rel=\"noreferrer noopener\">dedicated schema with strict permissions<\/a>, ownership, validation constraints, and clear cleanup rules.<br><br><\/li>\n\n\n\n<li><strong>Avoid global temporary procedures<\/strong>; do <em>not<\/em> let privileged code execute ## procedures by predictable name! Package helper logic in signed stored procedures or controlled permanent modules instead.<br><br><\/li>\n\n\n\n<li><strong>Use unpredictable names only as a fallback<\/strong>. A <a href=\"https:\/\/betterexplained.com\/articles\/the-quick-guide-to-guids\/\" target=\"_blank\" rel=\"noreferrer noopener\">GUID (globally unique identifier)<\/a> suffix <em>can<\/em> reduce object hijacking risk, but doesn&#8217;t fix the core design if lower-privileged sessions can still influence the object.<br><br><\/li>\n\n\n\n<li><strong>Validate before use. <\/strong>If a legacy workflow can&#8217;t be changed immediately, validate the <code>object_id<\/code>, <code>schema<\/code>, expected columns, expected data types, row source, and row values before privileged code consumes the object.<br><br><\/li>\n\n\n\n<li><strong>Review dynamic SQL. <\/strong>Never concatenate administrative commands directly from temporary-table rows. Use parameterized calls, <code><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/functions\/quotename-transact-sql?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">QUOTENAME<\/a><\/code> for identifiers, strict <a href=\"https:\/\/en.wikipedia.org\/wiki\/Whitelist\" target=\"_blank\" rel=\"noreferrer noopener\">allowlists\/whitelists<\/a>, and explicit validation.<\/li>\n<\/ul>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"h-information-disclosure-through-dmvs-and-metadata-visibility-in-sql-server\">Information disclosure through DMVs and metadata visibility in SQL Server<\/h2>\n\n\n\n<p><strong>DMVs, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/learn\/sql-server-system-views-the-basics\/\" target=\"_blank\" rel=\"noreferrer noopener\">catalog views<\/a>, traces, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/other\/t-sql-tuesday-181-query-store-and-its-evolution\/\" target=\"_blank\" rel=\"noreferrer noopener\">Query Store<\/a>, <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>, and job history are essential for troubleshooting. They&#8217;re also <em>excellent<\/em> reconnaissance tools. <\/strong><\/p>\n\n\n\n<p>While a login with broad observability rights may not have <code><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/the-basic-t-sql-select-statement\/#:~:text=The%20SELECT%20statement%20is%20the,and%20GROUP%20BY%20clauses%2C%20respectively.\" target=\"_blank\" rel=\"noreferrer noopener\">SELECT<\/a><\/code> permission on sensitive tables, it may still be able to learn database names, job names, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/how-to-create-a-sql-server-linked-server-to-oracle-26ai-free\/\" target=\"_blank\" rel=\"noreferrer noopener\">linked server<\/a> names, query text, parameter values, execution patterns, privileged account names, and application architecture. This matters because attackers rarely need full data access at the start of a campaign. <\/p>\n\n\n\n<p>Instead, they just need a map telling them which accounts are trusted, which jobs run as <code>sa<\/code>, which databases look sensitive, which procedures use <code><a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/t-sql\/statements\/execute-as-clause-transact-sql?view=sql-server-ver17&amp;tabs=sqlserver\" target=\"_blank\" rel=\"noreferrer noopener\">EXECUTE AS OWNER<\/a><\/code>, which linked servers exist, and which monitoring rules are watching them. Broad metadata visibility can provide that map.<\/p>\n\n\n\n<p>The risk increased in complexity with newer SQL Server versions, with some DMV access now split across more granular permissions (such as <code>VIEW SERVER PERFORMANCE STATE<\/code> in SQL Server 2022 onwards). It&#8217;s useful, yes, but doesn&#8217;t remove the need for review. <\/p>\n\n\n\n<p><strong>Simply put: a monitoring account should receive the smallest permission set necessary for the exact telemetry it needs.<\/strong><\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-an-example-of-this-in-practice\">An example of this in practice<\/h3>\n\n\n\n<p>A third-party monitoring account is granted <code>VIEW SERVER STATE<\/code> because the vendor dashboard needs to display active sessions and <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/sql-server-wait-events-taking-the-guesswork-out-of-performance-profiling\/\" target=\"_blank\" rel=\"noreferrer noopener\">wait<\/a> statistics. Later, the account is compromised through the application server that hosts the monitoring tool. The attacker can&#8217;t query the Finance database directly, but they <em>can<\/em> observe currently running requests and retrieve query text through DMVs.<\/p>\n\n\n\n<p>From that visibility, the attacker learns the name of a privileged password-reset procedure, sees which job checks for unauthorized sysadmins, discovers which linked server is used for financial reporting, and identifies the Windows group that the monitoring script treats as approved. <\/p>\n\n\n\n<p><strong>So, even if no data rows are stolen at this stage, the attacker has learned how the environment defends itself &#8211; and where to aim the next step.<\/strong><\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">\/* Review broad server-level observability permissions *\/\nSELECT\n    pr.name AS principal_name,\n    pr.type_desc,\n    pe.state_desc,\n    pe.permission_name\nFROM sys.server_permissions AS pe\nJOIN sys.server_principals AS pr\n    ON pe.grantee_principal_id = pr.principal_id\nWHERE pe.permission_name IN\n(\n    'VIEW SERVER STATE',\n    'VIEW SERVER PERFORMANCE STATE',\n    'VIEW ANY DEFINITION',\n    'ALTER ANY EVENT SESSION',\n    'ALTER TRACE',\n    'CONTROL SERVER'\n)\nORDER BY pr.name, pe.permission_name;\n\n\/* Review fixed server roles that indirectly imply broad visibility or control *\/\nSELECT\n    role_name = roles.name,\n    member_name = members.name,\n    members.type_desc\nFROM sys.server_role_members AS srm\nJOIN sys.server_principals AS roles\n    ON srm.role_principal_id = roles.principal_id\nJOIN sys.server_principals AS members\n    ON srm.member_principal_id = members.principal_id\nWHERE roles.name IN ('sysadmin', 'securityadmin', 'serveradmin')\nORDER BY roles.name, members.name;<\/pre><\/div>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-what-are-the-signs-a-dba-should-look-for-0\">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, reporting users, and vendor tools. Additionally, any <a href=\"https:\/\/learn.microsoft.com\/en-us\/windows-server\/identity\/ad-ds\/manage\/understand-security-groups\" target=\"_blank\" rel=\"noreferrer noopener\">AD (Active Directory)<\/a> groups granted <code>VIEW SERVER STATE<\/code>, <code>VIEW SERVER PERFORMANCE STATE<\/code>, <code>VIEW ANY DEFINITION<\/code>, <code>ALTER TRACE<\/code>, or <code>ALTER ANY EVENT SESSION<\/code>.<br><br><\/li>\n\n\n\n<li>Support accounts that can read job definitions, job history, query text, execution plans, Query Store data, or Extended Events output <em>without<\/em> a clear operational need.<br><br><\/li>\n\n\n\n<li>Monitoring jobs or DBA scripts that include sensitive server names, approved-admin lists, credentials, tokens, or detection logic in plain <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">T-SQL<\/a> text.<br><br><\/li>\n\n\n\n<li>Unusual spikes in <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/how-to-get-sql-server-data-conversion-horribly-wrong\/\" target=\"_blank\" rel=\"noreferrer noopener\">conversion errors<\/a>, divide-by-zero errors, or syntax errors against system views, which may indicate error-based metadata probing.<br><br><\/li>\n\n\n\n<li>Repeated calls to <code>sys.dm_exec_sql_text<\/code>, <code>sys.dm_exec_query_plan<\/code>, <code>sys.dm_exec_requests<\/code>, <code>fn_trace_gettable<\/code>, or Extended Events file readers from accounts that don&#8217;t usually perform troubleshooting.<\/li>\n<\/ul>\n<\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"h-what-to-monitor-and-how-0\">What to monitor &#8211; and how<\/h3>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><strong>Permission baseline. <\/strong>Store a baseline of server-level permissions and fixed server-role membership. Alert on new grants of broad observability permissions, especially to service accounts and domain groups.<br><br><\/li>\n\n\n\n<li><strong>Error probing<\/strong>. Use Extended Events to capture <code>error_reported<\/code> for conversion errors and divide-by-zero errors from low-privileged accounts. Then, correlate the <code>sql_text<\/code> with system object access.<br><br><\/li>\n\n\n\n<li><strong>Query text access<\/strong>. In high-security environments, monitor repeated access to query-text and plan-related DMVs. This can be noisy, so start with unusual principals rather than <em>all<\/em> DBA tooling.<br><br><\/li>\n\n\n\n<li><strong>Default trace and trace permissions. <\/strong>Review who has <code>ALTER TRACE<\/code> and who can read trace files. If default trace is not needed, move to controlled Extended Events sessions with explicit targets and retention.<br><br><\/li>\n\n\n\n<li><strong>Monitoring-script hygiene, <\/strong>reviewing what your own monitoring code reveals. If a user can see the text of your monitoring query, assume they can also see your allowlists and detection gaps!<\/li>\n<\/ul>\n<\/div>\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">\/* XE example: detect possible error-based metadata probing *\/\nCREATE EVENT SESSION [Watch_Metadata_Probing]\nON SERVER\nADD EVENT sqlserver.error_reported\n(\n    ACTION\n    (\n        sqlserver.session_id,\n        sqlserver.server_principal_name,\n        sqlserver.database_name,\n        sqlserver.client_hostname,\n        sqlserver.client_app_name,\n        sqlserver.sql_text\n    )\n    WHERE\n    (\n        ([error_number] = 245\n         OR [error_number] = 248\n         OR [error_number] = 257\n         OR [error_number] = 529\n         OR [error_number] = 8114\n         OR [error_number] = 8134)\n        AND [severity] &gt;= 11\n    )\n)\nADD TARGET package0.event_file\n(\n    SET filename = N'C:\\SQLAudit\\Watch_Metadata_Probing.xel',\n        max_file_size = 100,\n        max_rollover_files = 5\n);\nGO\nALTER EVENT SESSION [Watch_Metadata_Probing] ON SERVER STATE = START;\nGO\n\n\/* Find modules that may expose sensitive operational logic in visible text *\/\nSELECT\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\nFROM sys.sql_modules AS m\nJOIN sys.objects AS o\n    ON m.object_id = o.object_id\nWHERE m.definition LIKE '%password%'\n   OR m.definition LIKE '%secret%'\n   OR m.definition LIKE '%token%'\n   OR m.definition LIKE '%sysadmin%'\n   OR m.definition LIKE '%OPENQUERY%'\n   OR m.definition LIKE '%EXECUTE AS%'\nORDER BY schema_name, object_name;<\/pre><\/div>\n\n\n\n<div id=\"callout-block_d0edb91a2bf0b676f4d307da0a80d1c4\" 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 to remember<\/strong><\/p>\n\n\n\n<p>Not every monitoring account is dangerous, and not every DMV is sensitive in the same way. The problem is blanket visibility. Replace broad grants with task-specific access wherever possible, and assume that query text, execution plans, and job definitions may reveal security assumptions that attackers can reuse.<\/p>\n\n<\/div>\n<\/div> \n\n\n<h3 class=\"wp-block-heading\" id=\"h-recommended-fixes-or-mitigations\">Recommended fixes or mitigations<\/h3>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><strong>Apply least privilege to observability<\/strong>, granting <em>only<\/em> the permissions required for the specific monitoring function. Do not use <code>VIEW SERVER STATE<\/code> as the default answer to every monitoring request!<br><br><\/li>\n\n\n\n<li><strong>Use signed modules or controlled wrappers. <\/strong>When a user needs a small set of DMV-derived metrics, expose a sanitized result through a signed stored procedure instead of granting broad DMV access.<br><br><\/li>\n\n\n\n<li><strong>Separate monitoring tiers &#8211; <\/strong>a basic health dashboard should <em>not<\/em> use the same account as deep DBA diagnostics! Use separate accounts, separate permissions, and separate retention.<br><br><\/li>\n\n\n\n<li><strong>Remove secrets from T-SQL text. <\/strong>Never hardcode passwords, <a href=\"https:\/\/www.ibm.com\/docs\/en\/ias?topic=elements-tokens\" target=\"_blank\" rel=\"noreferrer noopener\">tokens<\/a>, <a href=\"https:\/\/learn.microsoft.com\/en-us\/azure\/storage\/common\/storage-sas-overview\" target=\"_blank\" rel=\"noreferrer noopener\">SAS (shared access signature)<\/a> URLs, sensitive hostnames, or detection allowlists in queries that can be observed by non-administrators.<br><br><\/li>\n\n\n\n<li><strong>Reduce trace exposure. <\/strong>Prefer controlled Extended Events sessions over default trace dependencies. Restrict file-system access to trace\/XE (Extended Events) targets.<br><br><\/li>\n\n\n\n<li><strong>Review error handling<\/strong> &#8211; and never echo sensitive internal values in custom error messages or <code><a href=\"https:\/\/learn.microsoft.com\/en-us\/dotnet\/csharp\/language-reference\/statements\/exception-handling-statements\" target=\"_blank\" rel=\"noreferrer noopener\">CATCH<\/a><\/code> blocks! Treat errors as a potential disclosure channel.<\/li>\n<\/ul>\n<\/div>\n\n\n<section id=\"my-first-block-block_038f019557d3a6bfeaa16aeb0a4c841e\" 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\">Move fast. Govern at scale.<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            Redgate Flyway Enterprise embeds guardrails in the database layer, so every change is policy-checked, deterministic, and traceable.                                    <\/div>\n            <\/div>\n                                            <a href=\"https:\/\/www.red-gate.com\/products\/flyway\/enterprise\/\" class=\"btn btn--secondary btn--lg\" aria-label=\"Try for free: Move fast. Govern at scale.\">Try for free<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<h2 class=\"wp-block-heading\" id=\"h-sql-server-audit-assurance-proving-your-audit-catches-indirect-access\">SQL Server audit assurance: proving your audit catches indirect access<\/h2>\n\n\n\n<p><strong><a href=\"https:\/\/www.red-gate.com\/simple-talk\/collections\/the-complete-guide-to-auditing-sql-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">Auditing<\/a> is the last line of defense when prevention fails &#8211; and is also one of the easiest controls to overestimate. Many environments can show that an audit exists, but far fewer can prove that it captures the actual ways attackers may access the data. The difference between audit configuration and audit assurance is <em>testing<\/em>.<\/strong><\/p>\n\n\n\n<p>A common blind spot appears when audit policies are written around a single access path. For example, a DBA may validate that <code>SELECT * FROM dbo.SecureVault<\/code> generates an audit record, then assume the table is covered. <\/p>\n\n\n\n<p>However, <em>real <\/em>access paths may involve <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/mastering-sql-views\/\" target=\"_blank\" rel=\"noreferrer noopener\">views<\/a>, synonyms, stored procedures, dynamic SQL, cross-database three-part names, sparse column sets, computed columns, indexed views, or application modules. If the audit was only tested against the obvious query, it may provide a false sense of security.<\/p>\n\n\n\n<p><code>SENSITIVE_BATCH_COMPLETED_GROUP<\/code> can be useful for classified data in SQL Server 2022 (and onwards), but should not be treated as a complete audit strategy by itself. Instead, layer it with schema object access auditing, permission-change auditing, module-execution monitoring, Extended Events, and regular adversarial validation.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-an-example-of-this-in-practice-0\">An example of this in practice<\/h3>\n\n\n\n<p>An organization classifies columns in <code>dbo.SecureVault<\/code> as highly confidential and enables an <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/security\/auditing\/create-a-server-audit-and-server-audit-specification?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">audit specification<\/a>. During validation, the DBA runs a direct <code>SELECT<\/code> from the same database context, sees the expected audit event, and the compliance task is marked as complete.<\/p>\n\n\n\n<p>Later, a compromised reporting account accesses the same data through a view, synonym, stored procedure, dynamic SQL, cross-database three-part name, or <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/learn\/the-xml-methods-in-sql-server\/\" target=\"_blank\" rel=\"noreferrer noopener\">XML<\/a> column set over sparse columns. <\/p>\n\n\n\n<p>Depending on how the audit was scoped and tested, the indirect path may produce different audit details, may be captured only at the wrapper level, or may not generate the alert the security team expects. The control may technically exist while still failing the detection goal.<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">\/* Inventory server audits and database audit specifications *\/\nSELECT\n    a.name AS audit_name,\n    a.is_state_enabled AS audit_enabled,\n    a.type_desc AS audit_target_type,\n    s.name AS database_spec_name,\n    s.is_state_enabled AS database_spec_enabled,\n    d.audit_action_name,\n    d.class_desc,\n    d.audited_result\nFROM sys.server_audits AS a\nLEFT JOIN sys.database_audit_specifications AS s\n    ON a.audit_guid = s.audit_guid\nLEFT JOIN sys.database_audit_specification_details AS d\n    ON s.database_specification_id = d.database_specification_id\nORDER BY a.name, s.name, d.audit_action_name;\n\n\/* Inventory server audit specifications *\/\nSELECT\n    a.name AS audit_name,\n    a.is_state_enabled AS audit_enabled,\n    s.name AS server_spec_name,\n    s.is_state_enabled AS server_spec_enabled,\n    d.audit_action_name,\n    d.audited_result\nFROM sys.server_audits AS a\nLEFT JOIN sys.server_audit_specifications AS s\n    ON a.audit_guid = s.audit_guid\nLEFT JOIN sys.server_audit_specification_details AS d\n    ON s.server_specification_id = d.server_specification_id\nORDER BY a.name, s.name, d.audit_action_name;<\/pre><\/div>\n\n\n\n<h3 class=\"wp-block-heading\" id=\"h-what-are-the-signs-a-dba-should-look-for-1\">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><strong>Audit specifications that rely on one action group only<\/strong>; no complementary coverage for schema access, permission changes, <a href=\"https:\/\/learn.microsoft.com\/en-us\/sql\/relational-databases\/security\/authentication-access\/database-level-roles?view=sql-server-ver17\" target=\"_blank\" rel=\"noreferrer noopener\">role membership<\/a> changes, or module execution.<br><br><\/li>\n\n\n\n<li><strong>Sensitive tables exposed through views, synonyms, stored procedures, functions, reporting schemas, computed columns, sparse column sets, or cross-database dependencies<\/strong>, while auditing is tested only against the base table.<br><br><\/li>\n\n\n\n<li><strong>Audits that are enabled but rarely reviewed<\/strong>, never restored from the audit target, and are not integrated with <a href=\"https:\/\/www.red-gate.com\/hub\/product-learning\/redgate-monitor\/integrating-redgate-monitor-into-a-tier-1-alert-and-notification-system\/\" target=\"_blank\" rel=\"noreferrer noopener\">alerting<\/a>.<br><br><\/li>\n\n\n\n<li><strong>Compliance validation based on the existence of an audit configuration<\/strong>, rather than adversarial testing of access paths.<br><br><\/li>\n\n\n\n<li><strong>Audit targets<\/strong> stored in locations that SQL Server service accounts, local administrators, or privileged database operators can tamper with, free from independent detection.<\/li>\n<\/ul>\n<\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"h-what-to-monitor-and-how-1\">What to monitor &#8211; and how<\/h3>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><strong>Audit the audit configuration. <\/strong>Monitor changes to audits and audit specifications. A disabled audit, changed predicate, changed target path, or dropped specification should alert immediately.<br><br><\/li>\n\n\n\n<li><strong>Test direct <em>and<\/em> indirect access paths<\/strong>. For each sensitive table, test direct <code>SELECT<\/code>, views, stored procedures, dynamic SQL, synonyms, cross-database access, reporting schemas, and sparse XML column sets where applicable.<br><br><\/li>\n\n\n\n<li><strong>Layer SQL Audit with XE<\/strong>. Use <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> for durable security evidence, coupled with Extended Events for additional operational context such as <code>session_id<\/code>, <code>client_app_name<\/code>, <code>hostname<\/code>, and SQL text patterns.<br><br><\/li>\n\n\n\n<li><strong>Validate alert semantics<\/strong>, and don&#8217;t just check that an audit row exists. Verify that the row contains enough information for the SOC (security operations center) or DBA team to understand what was accessed, by whom, from where, and through which path.<br><br><\/li>\n\n\n\n<li><strong>Review audit retention and integrity. <\/strong>Ensure audit files are written to protected storage, collected centrally, retained long enough, and reviewed for gaps.<\/li>\n<\/ul>\n<\/div>\n\n\n<h3 class=\"wp-block-heading\" id=\"h-the-access-paths-to-test-and-how-to-do-so\">The access paths to test &#8211; and how to do so<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Access path to test<\/strong><\/td><td><strong>Why it matters<\/strong><\/td><td><strong>Expected assurance outcome<\/strong><\/td><\/tr><tr><td>Direct <code>SELECT<\/code> on sensitive table<\/td><td>Baseline validation only. Not sufficient by itself.<\/td><td>Audit row includes principal, database, object, statement, time, client context, and result.<\/td><\/tr><tr><td>View over sensitive table<\/td><td>Many applications expose data through reporting views.<\/td><td>Audit either captures the base object access or clearly captures the wrapper object and statement.<\/td><\/tr><tr><td>Stored procedure wrapper<\/td><td>Applications may never query the base table directly.<\/td><td>Execution and underlying sensitive access are visible enough for investigation.<\/td><\/tr><tr><td>Dynamic SQL<\/td><td>Attackers often use dynamic execution to change shape and context.<\/td><td>Audit captures the submitted batch\/procedure text or complementary XE captures context.<\/td><\/tr><tr><td>Cross-database three-part name<\/td><td>Database-level audit scoping may be misunderstood.<\/td><td>Access from another database context still produces the expected security signal.<\/td><\/tr><tr><td>Synonym<\/td><td>Synonyms hide the target object and can obscure review.<\/td><td>Audit records are usable and identify the path or the resolved target clearly enough.<\/td><\/tr><tr><td>Sparse XML column set<\/td><td>Column-level expectations may not match aggregated XML access.<\/td><td>Sensitive data access still generates a detectable event or compensating alert.<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">\/* Example validation checklist for one sensitive object *\/\n\/* Run with an approved test login in a controlled window. *\/\n\n-- 1. Direct access\nSELECT TOP (1) * FROM SensitiveData.dbo.SecureVault;\n\n-- 2. Cross-database access from another context\nUSE master;\nSELECT TOP (1) * FROM SensitiveData.dbo.SecureVault;\n\n-- 3. Dynamic SQL\nEXEC(N'SELECT TOP (1) * FROM SensitiveData.dbo.SecureVault;');\n\n-- 4. Stored procedure wrapper - create\/use a controlled test wrapper\nEXEC SensitiveData.dbo.usp_TestReadSecureVault;\n\n-- 5. View or synonym path - where applicable\nSELECT TOP (1) * FROM Reporting.dbo.vw_SecureVault;\nSELECT TOP (1) * FROM Reporting.dbo.Syn_SecureVault;\n\n\/* After each test, confirm that your audit\/alert generated the event you expect. *\/\n\/* SQL Audit example: monitor changes to audit configuration *\/\nUSE master;\nGO\nCREATE SERVER AUDIT [Audit_Security_Configuration]\nTO FILE (FILEPATH = N'C:\\SQLAudit\\');\nGO\n\nCREATE SERVER AUDIT SPECIFICATION [Audit_Audit_Changes]\nFOR SERVER AUDIT [Audit_Security_Configuration]\nADD (AUDIT_CHANGE_GROUP),\nADD (SERVER_PERMISSION_CHANGE_GROUP),\nADD (SERVER_ROLE_MEMBER_CHANGE_GROUP),\nADD (DATABASE_PERMISSION_CHANGE_GROUP),\nADD (DATABASE_ROLE_MEMBER_CHANGE_GROUP)\nWITH (STATE = ON);\nGO\n\nALTER SERVER AUDIT [Audit_Security_Configuration] WITH (STATE = ON);\nGO<\/pre><\/div>\n\n\n\n<div id=\"callout-block_d0edb91a2bf0b676f4d307da0a80d1c4\" 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>Audit design principle<\/strong> <\/p>\n\n\n\n<p>A good audit strategy is defined by whether the security team can reliably answer <em>who<\/em> accessed the sensitive data, through <em>what<\/em> path, and from <em>which<\/em> client. Also: which principal did they use, did the action succeed, and would the event have still been captured if the attacker had used an indirect path?<\/p>\n\n<\/div>\n<\/div> \n\n\n<h3 class=\"wp-block-heading\" id=\"h-recommended-fixes-or-mitigations-0\">Recommended fixes or mitigations<\/h3>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><strong>Use layered audit coverage. <\/strong>Combine sensitive-data audit groups with schema object access, schema object changes, permission changes, database role membership changes, server role membership changes, and audit-change monitoring where appropriate.<br><br><\/li>\n\n\n\n<li><strong>Design for indirect access. <\/strong>Audit wrappers, views, synonyms, procedures, and reporting schemas that expose sensitive data.<br><br><\/li>\n\n\n\n<li><strong>Create an audit validation suite<\/strong> with a <em>repeatable<\/em> set of test queries for each critical data set. Run it after schema changes, permission changes, SQL Server upgrades, and audit policy changes.<br><br><\/li>\n\n\n\n<li><strong>Protect audit targets<\/strong> by writing audit files to protected storage and restricting operating system (OS)-level access. Also forward audit events to a central location where DBAs and SQL service accounts cannot silently modify history.<br><br><\/li>\n\n\n\n<li><strong>Alert on audit tampering<\/strong>. Audit disablement, audit target changes, specification changes, and audit write failures should trigger high-priority alerts.<br><br><\/li>\n\n\n\n<li><strong>Document expected gaps<\/strong>. If a control is compensating rather than complete, document what it does <em>not<\/em> catch &#8211; and what secondary control covers that gap.<\/li>\n<\/ul>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"h-in-summary-a-dba-s-checklist-for-securing-temporary-objects-metadata-visibility-and-audit-assurance-nbsp-in-sql-server\">In summary: a DBA&#8217;s checklist for securing temporary objects, metadata visibility, and audit assurance&nbsp;in SQL Server<\/h2>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>Review all ## usage in SQL Agent jobs, stored procedures, vendor scripts, and application code.<br><br><\/li>\n\n\n\n<li>Remove global temporary objects from privileged workflows or replace them with local temp objects or secured staging tables.<br><br><\/li>\n\n\n\n<li>Review all grants of <code>VIEW SERVER STATE<\/code>, <code>VIEW SERVER PERFORMANCE STATE<\/code>, <code>VIEW ANY DEFINITION<\/code>, <code>ALTER TRACE<\/code>, and <code>ALTER ANY EVENT SESSION<\/code>.<br><br><\/li>\n\n\n\n<li>Review who can read query text, execution plans, job steps, Extended Events files, trace files, and Query Store data.<br><br><\/li>\n\n\n\n<li>Monitor conversion\/divide-by-zero error bursts against system objects as possible metadata probing.<br><br><\/li>\n\n\n\n<li>Remove secrets, tokens, approved-admin allowlists, and sensitive server names from observable T-SQL text.<br><br><\/li>\n\n\n\n<li>Inventory audit specifications and confirm that sensitive-data access is tested through direct and indirect paths.<br><br><\/li>\n\n\n\n<li>Alert on audit configuration changes and ensure audit targets are protected outside the control of the SQL Server service account.<br><br><\/li>\n\n\n\n<li>Treat monitoring and auditing as code: version it, test it, review it, and re-test it after every meaningful change.<\/li>\n<\/ul>\n<\/div>\n\n\n<h2 class=\"wp-block-heading\" id=\"h-conclusion-the-key-takeaways\">Conclusion (the key takeaways)<\/h2>\n\n\n\n<p><strong>A global temporary object is not private just because it is temporary, a monitoring permission is not harmless just because it is read-only, and an audit specification is not sufficient just because it exists. Each one must be reviewed in the context of execution scope, privilege, observability, and validation.<\/strong><\/p>\n\n\n\n<p>As proven in every part of this series, attackers succeed by chaining ordinary features together. Default permissions, privileged roles, <code>msdb<\/code> jobs, untrusted restores, cross-database trust, triggers, linked servers, temporary objects, DMVs, and audit blind spots are all connected trust boundaries. It only takes one small weakness, in one area, to create danger, as another trusted component may amplify it.<\/p>\n\n\n\n<p><strong>For DBAs, the message is simple: reduce privilege, shared scope, and unnecessary metadata visibility.<\/strong> <strong>You should also prevent privileged code consuming untrusted input, and treat an audit as a system that must be tested.<\/strong> <\/p>\n\n\n\n<p><strong>SQL Server <em>can<\/em> be secured effectively &#8211; but only when DBAs move from trusting defaults to continuously verifying what the environment actually allows.<\/strong><\/p>\n\n\n\n<section id=\"my-first-block-block_2004c344fef487b18914cf82c3aea570\" 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<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs: How to secure temporary objects, metadata visibility, and audit assurance\u00a0in SQL Server<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. What&#039;s the risk with global temporary tables and procedures in SQL Server?<\/h3>\n            <div class=\"faq-answer\">\n                <p>Objects prefixed with <code class=\"bg-text-200\/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]\">##<\/code> are visible outside the session that created them. If a privileged process (like an sa-owned SQL Agent job) trusts a predictable global object, a lower-privileged attacker can pre-create it, poison its data, or \u2014 in the case of global temp procedures \u2014 control the executable code itself.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. Why are DMVs considered a security risk if they&#039;re read-only?<\/h3>\n            <div class=\"faq-answer\">\n                <p class=\"font-claude-response-body break-words whitespace-normal\" dir=\"ltr\">DMVs don&#8217;t expose table data, but broad access (e.g., <code class=\"bg-text-200\/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]\">VIEW SERVER STATE<\/code>) can reveal job names, query text, linked servers, and privileged account names. This metadata gives attackers a reconnaissance map of trust boundaries even without direct data access.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. How do I know if my SQL Server audit is actually working?<\/h3>\n            <div class=\"faq-answer\">\n                <p>Test more than the obvious path. An audit validated only against a direct <code class=\"bg-text-200\/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]\">SELECT<\/code> on a sensitive table may miss access through views, synonyms, stored procedures, dynamic SQL, or cross-database three-part names \u2014 all of which can bypass the assumptions baked into the original audit design.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">4. What&#039;s the single most important mindset shift for DBAs here?<\/h3>\n            <div class=\"faq-answer\">\n                <p class=\"font-claude-response-body break-words whitespace-normal\" dir=\"ltr\">Treat these features as operational trust boundaries, not neutral defaults. Ask who can see an object, who can write to it, whether privileged code validates it before use, and whether your audit was tested against indirect access paths \u2014 not just the expected one.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">5. Where should I start if I only have time to check one thing?<\/h3>\n            <div class=\"faq-answer\">\n                <div role=\"feed\" aria-label=\"Chat messages\" aria-describedby=\"_r_ep_\" 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\">Run a search for <code class=\"bg-text-200\/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]\">##<\/code> references across <code class=\"bg-text-200\/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]\">msdb.dbo.sysjobsteps<\/code> and <code class=\"bg-text-200\/5 border border-0.5 border-border-300 text-danger-000 whitespace-pre-wrap rounded-[0.4rem] px-1 py-px text-[0.9rem]\">sys.sql_modules<\/code>. Global temp objects inside privileged jobs are one of the most concrete, checkable risks covered here.<\/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>Global temp objects, DMVs, and audit gaps can quietly become attack paths in SQL Server. Learn what to review, monitor, and fix as a DBA.&hellip;<\/p>\n","protected":false},"author":65554,"featured_media":103349,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143523,53,143530,46,143524,143531],"tags":[4168,4170,159408,5765,4150,4151,4252],"coauthors":[6809],"class_list":["post-112093","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","category-t-sql-programming-sql-server","tag-database","tag-database-administration","tag-fasqlsecurity","tag-security-and-compliance","tag-sql","tag-sql-server","tag-t-sql-programming"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/112093","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=112093"}],"version-history":[{"count":10,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/112093\/revisions"}],"predecessor-version":[{"id":112173,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/112093\/revisions\/112173"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media\/103349"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=112093"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=112093"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=112093"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=112093"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}