PowerShell Constrained Language Mode

Constrained Language Mode (CLM) is the PowerShell language tier that actually is a security boundary — unlike the execution policy, which Microsoft describes as a safety rail. Introduced in PowerShell 3.0, CLM strips the dangerous parts of the language: Add-Type, arbitrary .NET type access, COM objects, and direct Windows API calls are all blocked, leaving only an allowlist of “safe” core types ([string], [int], [datetime], [hashtable], [xml], [adsi], [wmi], and a few dozen others) whose properties and methods remain usable.

How it gets enabled

CLM is not something an administrator toggles directly. Per about_Language_Modes, PowerShell automatically drops into ConstrainedLanguage when it detects a system application-control policy — AppLocker or Windows Defender Application Control (WDAC, now “App Control for Business”):

  • Any script, module, or manifest allowed by the policy runs with FullLanguage rights.
  • Any script not allowed still runs, but only in ConstrainedLanguage.
  • If any WDAC UMCI policy with script enforcement is active — even in audit mode — interactive PowerShell is forced into CLM. Full interactive language requires disabling script enforcement on all active policies.

Check the current mode:

$ExecutionContext.SessionState.LanguageMode
# FullLanguage  → unconstrained
# ConstrainedLanguage → CLM active

Microsoft’s guidance is emphatic: CLM is only trustworthy under System Lockdown with App Control for Business. Without a lockdown policy, CLM can be bypassed (e.g., by spawning a new runspace or exploiting allowed-type quirks), so it must be paired with the policy that enforces it.

Offensive implications

CLM breaks the standard “download into memory and IEX” playbook: cradles that rely on New-Object Net.WebClient, reflection, or in-memory compilation fail because those types aren’t in the allowlist. The typical red-team adaptations:

  1. Script-as-application, not module: restructure tooling so functions are invoked by running the PS1 file directly rather than dot-sourcing/loading into memory — allowed scripts permitted by the AppLocker/WDAC policy run with FullLanguage rights.
  2. Write to disk in an allowed directory: execution from disk means the file hits Microsoft Defender’s on-access scanning, and AppLocker rules dictate which directories are trusted. Enumerate the effective policy to find writable + executable paths:
Get-AppLockerPolicy -Effective | Select-Object -ExpandProperty RuleCollections

The trade-off is a classic evasion triangle: CLM pushes payloads to disk → disk triggers Defender/AMSI → AMSI bypass (T1562.001) re-enables in-memory execution only if FullLanguage can be recovered. See invoke-webrequest-download-cradles for how the download side interacts with AMSI.

MITRE ATT&CK lists PowerShell Constrained Language mode explicitly as mitigation M1038 (Execution Prevention) for T1059.001 — one of the few controls the framework calls out by name for PowerShell abuse.

Detection

  • PowerShell EID 4103/4104 logs expose $ExecutionContext.SessionState.LanguageMode probes and failed type-resolution errors characteristic of tooling breaking under CLM
  • AppLocker EID 8004 / WDAC EID 3077 (script blocked) or 3076 (audit-mode would-block) reveal both attempted script execution and which directories attackers are probing
  • Get-AppLockerPolicy -Effective execution by non-admin tooling is itself a strong recon signal

Sources