Extended procedures (xp_*) / surface area

Status: Reviewed
Stack: PowerShell → SQL / stored procs → Blazor
Depends on: SQL-CLI-09 (overview), SQL-CLI-01 (calling), SQL-CLI-09a (show advanced / Agent XPs / Ole Automation / xp_cmdshell config), SQL-CLI-07 (pipeline shapes)
Goal: Treat surface-area extended procs as policy, not convenience — know enablement via sp_configure, when any xp_* is justified, and the safer outsides (PowerShell on the box, Agent CmdExec + proxy, careful CLR, External Scripts).

Sources: Microsoft Learn (sql-server-ver17 / 2026). Expands SQL-CLI-09 §3.6 only — do not treat this as a rewrite of the overview. Config write path detail: SQL-CLI-09a. Calling: SQL-CLI-01. Staging → Blazor: SQL-CLI-07.


1. Scope map — surface-area XPs vs safer alternatives

In this doc (09e) Out → sibling / outside
Enablement policy for xp_cmdshell, Ole Automation, related gates Full sp_configure / RECONFIGURE mechanics → 09a
Inventory-ish XPs: xp_dirtree / xp_fixeddrives / xp_fileexist (caveats) Object help / space → 09b
xp_logininfo (Windows group expansion) Principals / orphans / grants → 09c / auth → 06
xp_readerrorlog / sp_readerrorlog for CLI log scrape Agent job control plane → 09d
Database Mail XPs / Agent XPs as configure gates only Mail profiles / Agent jobs how-to → Learn / 09d
Hardening checklist + Blazor “never expose shell” Pipeline Collect→Stage→Blazor → 07

Default posture for Rick’s AD/Windows shop: OS work stays in PowerShell on a jump box / the host, or in SQL Agent CmdExec / PowerShell job steps with a least-privilege proxy. The engine stores and transforms data; it is not a remote shell.

Surface Prefer instead
xp_cmdshell PS remoting / local PS; Agent CmdExec or PowerShell subsystem + proxy
sp_OA* (Ole Automation) .NET / PowerShell COM; or modern APIs — do not enable Ole Automation for new work
Filesystem inventory XPs Host PS (Get-ChildItem, Test-Path, CIM); newer DMFs where available (sys.dm_os_file_exists, …)
Drive free space (xp_fixeddrives) Monitoring / PS CIM / Get-Volume; not a Blazor collector
Log scrape sp_readerrorlog / xp_readerrorlog OK for ops CLI; ship logs with a real agent for fleets
Windows group expand xp_logininfo for triage; AD tooling / catalog for inventory (see 09c/06)
In-engine scripting External Scripts / careful CLR only with signed policy — still wider than “do it in PS outside”

2. Decision table: when ANY xp_* is justified

Situation Decision Why
New feature needs “run dir / copy / whoami from T-SQL” Refuse — do it outside the engine CIS/auditors flag xp_cmdshell = 1; blast radius = host
Legacy vendor requires xp_cmdshell and cannot change Break-glass: enable only for the task window; proxy + audit; ticket Learn: newly developed code shouldn’t use it; leave disabled
One-off DBA triage of ERRORLOG from sqlcmd OKsp_readerrorlog / xp_readerrorlog Documented read path; no surface-area toggle
Expand “who is in this Windows group login?” OK triagexp_logininfo; not fleet Blazor Needs DC; global groups only (not universal)
Check backup file exists before restore (Agent step) Prefer PS Test-Path or Agent step; xp_fileexist / DMF if already in T-SQL gate Undocumented XP vs supported DMF / outside
Blazor app pool “needs shell for convenience” Hard refuse App identity must never reach OS shell via SQL
Need scheduled OS task with SQL trigger Agent job (CmdExec/PS + proxy) or external scheduler Controllable creds, history, no instance-wide shell
Need COM / HTTP from T-SQL Refuse Ole Automation; use outside worker Ole Automation Procedures default off for a reason
Database Mail / Agent node in SSMS Enable gate options only (Database Mail XPs / Agent XPs) Gates ≠ “turn on cmdshell”

Rule: If the work can run as PowerShell under a constrained identity without widening the SQL surface, do that. Enable advanced surface options only with a written exception.


3. Curated procs & enablement policy

All sp_configure writes need ALTER SETTINGS (sysadmin / serveradmin hold it). Pattern always:

USE master;
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
-- set the option …
RECONFIGURE;
EXEC sp_configure 'show advanced options', 0;  -- steady-state: leave advanced off
RECONFIGURE;

Confirm with sys.configurations (value / value_in_use / is_dynamic) — see 09a. These surface options are dynamic (no restart) unless Learn says otherwise for a specific option.

3.1 xp_cmdshell — highest risk

Purpose Spawn a Windows command shell; return output as rows (nvarchar(255)), or NO_OUTPUT
Default Disabled (xp_cmdshell = 0)
Enable show advanced optionsEXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;
Who runs as sysadmin / CONTROL SERVER: SQL Server service account. Non-sysadmin: credential ##xp_cmdshell_proxy_account## via sp_xp_cmdshell_proxy_account — if missing, call fails
Grant for non-sysadmin User must exist in master; GRANT EXECUTE ON xp_cmdshell TO [login_or_user];
Policy Prefer never. If forced: enable only for the duration of the task, least-privilege proxy, audit, then disable. Prefer Agent CmdExec/PS + proxy or PS remoting
Ops note Synchronous; failure fails the batch. Can trip security audit tools. whoami via cmdshell reveals the token in use

Break-glass enable (then disable):

USE master;
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;
-- … minimal work …
EXEC sp_configure 'xp_cmdshell', 0; RECONFIGURE;
EXEC sp_configure 'show advanced options', 0; RECONFIGURE;

Proxy (only if non-sysadmin must call it): create a locked-down domain/local account → EXEC sp_xp_cmdshell_proxy_account N'DOMAIN\LeastPriv', N'<password>'; — treat the password like any secret; rotate; do not leave broad GRANT EXEC hanging around.

3.2 Ole Automation procedures (sp_OA*) — prefer not

Gate option Ole Automation Procedures — default 0
Enable Advanced on → EXEC sp_configure 'Ole Automation Procedures', 1; RECONFIGURE;
Effect Allows sp_OACreate / related to instantiate OLE objects in T-SQL
Policy Prefer never for new work. Legacy COM-from-SQL is a security and support sink. Replace with PowerShell / app-tier code

3.3 xp_dirtree / xp_fixeddrives / xp_fileexist — inventory usefulness vs caveats

| Proc | Usefulness | Caveat | | --- | --- | | xp_fixeddrives | Quick interactive free-MB per drive letter | Undocumented — fine for DBA peek; do not build Blazor/fleet contracts on it | | xp_dirtree | Recursive directory listing under a server path | Undocumented; host filesystem visibility; empty/limited for non-privileged callers | | xp_fileexist | File / parent-dir existence check | Undocumented; prefer sys.dm_os_file_exists (documented DMF where available) or PS Test-Path |

Policy: useful for interactive ops and some Agent pre-checks; never a public API for the app pool. Prefer host PowerShell or supported DMFs for anything that feeds dashboards.

3.4 xp_readerrorlog / sp_readerrorlog — still useful for CLI log scrape

Documented wrapper sys.sp_readerrorlog — filter SQL Server or Agent error logs by archive index + keywords
Args (sp_) @p1 log index (0 = current), @p2 product (1 SQL Server, 2 Agent), @p3/@p4 filter strings
Perms ≤2019: VIEW SERVER STATE. 2022+: VIEW ANY ERROR LOG or VIEW SERVER PERFORMANCE STATE
Policy Allowed for ops CLI triage. For fleets, ship ERRORLOG with a real collector — don’t poll giant logs into Blazor
EXEC sp_readerrorlog;                          -- current SQL log
EXEC sp_readerrorlog 1, 2;                     -- previous Agent log
EXEC sp_readerrorlog 0, 1, N'database', N'start';

(xp_readerrorlog is the related extended proc many scripts still call; prefer the documented sp_readerrorlog in new scripts.)

3.5 xp_logininfo — Windows group expansion

Purpose Windows user/group permission paths; @option = 'members' expands next-level group members
Args @acctname (fully qualified, e.g. CONTOSO\Group), @option (all | members), optional @privilege OUTPUT
Limits Global groups only — not universal. Needs DC contact for members; empty if DC unreachable
Perms CONTROL SERVER, or user in master with EXECUTE on xp_logininfo
Policy Ops triage OK. Point inventory / AD truth at 09c / 06 — don’t scrape this into Blazor as source of truth
EXEC xp_logininfo N'BUILTIN\Administrators';
EXEC xp_logininfo N'CONTOSO\SqlAdmins', N'members';

3.6 Database Mail XPs / Agent XPs — configure gates (pointer)

Option Default Meaning Deep dive
Agent XPs 0 Enables Agent extended procs; SSMS Agent node needs this (SSMS start of Agent enables automatically). Does not start the Agent service 09a / 09d
Database Mail XPs 0 Enables Database Mail; setting 0 prevents Mail from starting (running Mail may finish idle lifetime) 09a + Learn Database Mail — not a full how-to here
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'Agent XPs', 1; RECONFIGURE;           -- gate only
EXEC sp_configure 'Database Mail XPs', 1; RECONFIGURE;   -- gate only

These are legitimate surface toggles when you actually run Agent or Mail — they are not excuses to enable xp_cmdshell or Ole Automation.


4. Permissions + surface-area hardening checklist

Control Expectation
Who can sp_configure ALTER SETTINGS only — tiny group
xp_cmdshell value_in_use = 0 unless ticketed break-glass
Ole Automation 0 unless legacy exception on file
GRANT EXECUTE on xp_cmdshell None for app/roles; revoke leftovers
Proxy credential Absent unless required; least privilege; secret hygiene
App / Blazor pool login No CONTROL SERVER; no EXEC on toxic XPs
Filesystem XPs / DMFs Deny broad EXEC/SELECT to non-DBA roles if SSMS browse is a concern
Audit Alert on sp_configure changes to surface options; review ERRORLOG / audit spec
Steady state show advanced options = 0 after change windows
CIS / baseline Treat xp_cmdshell = 1 as finding until exception documented

Quick inventory (Blazor-friendly single set):

SELECT name, value, value_in_use, is_dynamic, is_advanced
FROM sys.configurations
WHERE name IN (
  N'xp_cmdshell',
  N'Ole Automation Procedures',
  N'Agent XPs',
  N'Database Mail XPs',
  N'clr enabled',
  N'Ad Hoc Distributed Queries',
  N'show advanced options'
);

5. Gotchas & anti-patterns (AD/Windows ops)

  1. “We’ll leave cmdshell on; it’s just the lab.” — Labs become prod patterns; auditors don’t care.
  2. Sysadmin runs cmdshell as the service account — often Local System / high-priv domain account → host takeover class risk.
  3. Granting EXEC without a proxy — non-sysadmin then fails or you widen who is sysadmin instead. Wrong either way.
  4. Ole Automation for “HTTP from SQL” — ancient pattern; replace with outside worker.
  5. Building dashboards on xp_fixeddrives / xp_dirtree — undocumented shapes break; use PS/monitoring/DMFs.
  6. xp_logininfo for universal groups — returns incomplete AD truth; you’ll mis-read membership.
  7. Enabling Agent XPs and assuming Agent is running — gate ≠ service. Check the Windows/SQL Agent service (09d).
  8. Blazor or SSRS “helper” proc that wraps xp_cmdshell — indefensible; move to Agent/PS.
  9. Forgetting to disable after break-glass — automate the off path in the same change ticket.
  10. Azure SQL Database — no classic Agent / classic surface model like on-box SQL; don’t copy-paste enable scripts expecting the same world (MI closer to box, still validate).

6. Blazor: never expose xp_cmdshell from the app pool

Rule Detail
No shell from UI App pool / managed identity must not EXEC xp_cmdshell or call a wrapper that does
Inventory only Grid = sys.configurations surface-option slice (§4) — read-only collector
No OA / dirtree from app Same trust-boundary argument
Multi-set trap Same as 09b–09d: don’t scrape help/XP text dumps with Invoke-Sqlcmd for grids
Remediation UX Blazor may show “xp_cmdshell enabled” as a red finding; remediation stays in ops runbook / PS, not a button that toggles prod surface

Staging shape sketch:

SELECT @@SERVERNAME AS ServerName, name AS OptionName,
       CAST(value AS int) AS ConfigValue,
       CAST(value_in_use AS int) AS RunValue,
       SYSUTCDATETIME() AS CollectedUtc
FROM sys.configurations
WHERE name IN (N'xp_cmdshell', N'Ole Automation Procedures',
               N'Agent XPs', N'Database Mail XPs');

7. Recipes (detection / hardening + labeled break-glass)

R1 — Surface-area inventory (sqlcmd)

sqlcmd -S "$SQL_SERVER" -E -d master -b -Q "
SET NOCOUNT ON;
SELECT name, value, value_in_use
FROM sys.configurations
WHERE name IN (N'xp_cmdshell', N'Ole Automation Procedures',
  N'Agent XPs', N'Database Mail XPs', N'show advanced options')
ORDER BY name;"

R2 — Assert cmdshell & Ole are off (gate check)

sqlcmd -S "$SQL_SERVER" -E -d master -b -Q "
IF EXISTS (SELECT 1 FROM sys.configurations
           WHERE name = N'xp_cmdshell' AND value_in_use <> 0)
  THROW 50001, 'xp_cmdshell is enabled', 1;
IF EXISTS (SELECT 1 FROM sys.configurations
           WHERE name = N'Ole Automation Procedures' AND value_in_use <> 0)
  THROW 50002, 'Ole Automation Procedures is enabled', 1;"

R3 — Disable xp_cmdshell (hardening)

sqlcmd -S "$SQL_SERVER" -E -d master -b -Q "
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 0; RECONFIGURE;
EXEC sp_configure 'show advanced options', 0; RECONFIGURE;"

R4 — Disable Ole Automation

sqlcmd -S "$SQL_SERVER" -E -d master -b -Q "
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'Ole Automation Procedures', 0; RECONFIGURE;
EXEC sp_configure 'show advanced options', 0; RECONFIGURE;"

R5 — ERRORLOG keyword scrape (allowed)

sqlcmd -S "$SQL_SERVER" -E -d master -b -Q \
  "EXEC sp_readerrorlog 0, 1, N'Login failed';"

R6 — Windows group members triage (xp_logininfo)

sqlcmd -S "$SQL_SERVER" -E -d master -b -Q \
  "EXEC xp_logininfo N'CONTOSO\SqlAdmins', N'members';"

R7 — Safer alternative: Agent CmdExec posture (prefer over cmdshell)

Use an Agent job step subsystem Operating system (CmdExec) or PowerShell, run as a proxy mapped to a least-privilege credential — see 09d for start/help procs and roles. Do not enable xp_cmdshell to “make the same thing easier.”

R8 — Safer alternative: PowerShell on the box (drive / path inventory)

# Run on the SQL host or via remoting — not via xp_cmdshell
Get-Volume | Select-Object DriveLetter, FileSystemLabel, SizeRemaining, Size
Test-Path -Path 'D:\Backups\AppDb.bak'

R9 — BREAK-GLASS enable xp_cmdshell, whoami, disable

# Ticketed exception only — enable → prove token → disable in same window
sqlcmd -S "$SQL_SERVER" -E -d master -b -Q "
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;
EXEC xp_cmdshell 'whoami.exe';
EXEC sp_configure 'xp_cmdshell', 0; RECONFIGURE;
EXEC sp_configure 'show advanced options', 0; RECONFIGURE;"

R10 — BREAK-GLASS controlled command with return code (then disable)

-- Only inside a change window; disable when done (R3 / R9 pattern)
DECLARE @result int;
EXEC sp_configure 'show advanced options', 1; RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;
EXEC @result = xp_cmdshell N'dir D:\Backups\*.bak', NO_OUTPUT;
SELECT @result AS CmdShellReturnCode;  -- 0 success, 1 failure
EXEC sp_configure 'xp_cmdshell', 0; RECONFIGURE;
EXEC sp_configure 'show advanced options', 0; RECONFIGURE;

Prefer R7/R8. R9/R10 exist so ops has a documented break-glass — not a habit.


8. Pointers to 09 / 09a–d

Doc Covers
SQL-CLI-09 Overview: call patterns, procs vs DMVs, curated catalog (§3.6 xp_* warning)
SQL-CLI-09a sp_configure / RECONFIGURE; show advanced; option table (cmdshell / Ole / Mail / Agent gates)
SQL-CLI-09b Object help, space, text, rename; multi-set trap
SQL-CLI-09c Logins / users / roles / orphans; xp_logininfo in security context
SQL-CLI-09d Agent control plane vs msdb tables; prefer CmdExec/proxy over cmdshell
09e (this) Surface-area XPs — enablement policy, hardening, safer alternatives, recipes

Related stack: SQL-CLI-01 (calling / -b), SQL-CLI-06 (Windows auth / groups), SQL-CLI-07 (Collect→Stage→Blazor).

Learn anchors (ver17): xp_cmdshell (Transact-SQL), Server configuration: xp_cmdshell, sp_xp_cmdshell_proxy_account, Server configuration: Ole Automation Procedures, Server configuration: Agent XPs, Server configuration: Database Mail XPs, sp_readerrorlog, xp_logininfo, Surface area configuration, sys.configurations, sys.dm_os_file_exists (prefer over undocumented file XPs where available).