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.
| 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” |
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 | OK — sp_readerrorlog / xp_readerrorlog |
Documented read path; no surface-area toggle |
| Expand “who is in this Windows group login?” | OK triage — xp_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.
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.
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 options → EXEC 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.
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 |
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.
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.)
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';
| 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.
| 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'
);
xp_fixeddrives / xp_dirtree — undocumented shapes break; use PS/monitoring/DMFs.xp_logininfo for universal groups — returns incomplete AD truth; you’ll mis-read membership.xp_cmdshell — indefensible; move to Agent/PS.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');
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;"
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;"
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;"
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;"
sqlcmd -S "$SQL_SERVER" -E -d master -b -Q \
"EXEC sp_readerrorlog 0, 1, N'Login failed';"
xp_logininfo)sqlcmd -S "$SQL_SERVER" -E -d master -b -Q \
"EXEC xp_logininfo N'CONTOSO\SqlAdmins', N'members';"
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.”
# 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'
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;"
-- 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.
| 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).