PowerShell Constrained Language Mode: Architecture, Trust Boundaries, and Operational Reality
Constrained Language Mode filters script capabilities at the engine level — not a process sandbox. Understand its allowlist, trust assumptions, diagnostic checks, and the failure modes that break real automation before you rely on it as a security boundary.
07 Jul 2025, 12:59 UTC

The Problem: Script Execution Without Process Isolation
Organizations need to run untrusted or semi-trusted PowerShell scripts — automation from vendors, intern-contributed tooling, or delegated administration — without granting full .NET framework access. The traditional answer, separate accounts or restricted runspaces, adds operational overhead and still leaves a wide attack surface. Constrained Language Mode (CLM) offers a lighter-weight alternative: a single-process sandbox enforced by the PowerShell engine itself. The takeaway: CLM works as a capability filter for scripts you control, but it is not a security boundary against a compromised host or misconfigured policy.
Requirements Driving the Design
- Run scripts signed by approved publishers (AppLocker/WDAC) while blocking arbitrary .NET reflection, COM interop, and process creation.
- Preserve core language constructs — loops, conditionals, variables, functions — so logic-heavy automation still works.
- Allow a curated set of cmdlets and .NET types sufficient for data transformation, filtering, and serialization.
- Enforce the restriction at engine level so it cannot be disabled from within the session.
Smallest Suitable Design: Engine-Enforced Allowlist
CLM activates when the __PSLockdownPolicy environment variable is set or when AppLocker/Windows Defender Application Control (WDAC) marks a script as constrained. The engine then:
- Blocks
New-Object,Activator.CreateInstance,Add-Type, reflection APIs, COM/WMI/CIM cmdlets,Start-Process, and direct .NET method calls on non-whitelisted objects. - Exposes a fixed allowlist of .NET types:
String,Int32,DateTime,Hashtable,PSCustomObject, arrays, and a handful of others. - Permits only whitelisted cmdlets — roughly 100 vs. 1,500+ in FullLanguage — including
Get-Command,Where-Object,Select-Object,Sort-Object,Get-Content,Set-Content, and host-exposed APIs like$ExecutionContext.InvokeCommand.
No separate process, no AppContainer, no hypervisor. The same powershell.exe host runs both FullLanguage and ConstrainedLanguage sessions; the difference is a flag checked at every sensitive operation.
Trust and Data Boundaries
Host Is Trusted, Script Is Not
CLM assumes the host process (or the system policy that launched it) is trustworthy. A compromised host can simply unset __PSLockdownPolicy or spawn a new FullLanguage runspace. CLM does not isolate memory, handles, or tokens.
Data Crossing the Boundary
Objects returned from allowed cmdlets or constructed from whitelisted types cross freely. Arbitrary object graphs — e.g., a System.Data.DataTable or a custom class from Add-Type — cannot be instantiated or passed in. Serialization must use allowed types: hashtables, arrays, strings, primitives. If your automation needs rich objects, you must serialize to JSON/CSV in a FullLanguage wrapper and deserialize in CLM using ConvertFrom-Json (allowed) into PSCustomObject graphs.
Operational Verification: Diagnostics You Can Run Today
Run these checks in the target session (requires no elevation, only the ability to start PowerShell under the policy you're testing):
# 1. Confirm language mode
$ExecutionContext.SessionState.LanguageMode
# Expected: ConstrainedLanguage
# 2. Count available cmdlets
Get-Command -CommandType Cmdlet | Measure-Object
# Typical CLM: ~100; FullLanguage: 1500+
# 3. Verify blocked operation throws
Add-Type -TypeDefinition 'public class Test {}'
# Must throw PSSecurityException
# 4. Inspect effective AppLocker/WDAC policy (run elevated)
Get-AppLockerPolicy -Effective -Xml
# Or for DSC-based lockdown:
Get-CimInstance -Namespace root/Microsoft/Windows/DesiredStateConfiguration -ClassName MSFT_DSCLocalConfigurationManager
If step 1 returns FullLanguage, the policy is not applied. If step 3 succeeds (no exception), CLM is not enforced.
Failure Modes: Where Scripts Break
| Pattern | FullLanguage | ConstrainedLanguage | Mitigation |
|---|---|---|---|
Add-Type / reflection | Works | PSSecurityException | Rewrite using allowed types; move native interop to a signed FullLanguage helper |
| Third-party modules (e.g., AWS, Azure, Exchange) | Load | Fail on import or first blocked call | Use Microsoft-signed modules designed for CLM; test with Import-Module -ErrorAction Stop |
Start-Process, Get-WmiObject, COM | Works | Blocked | Replace with allowed cmdlets (Get-CimInstance is blocked; use Get-Command to discover alternatives) |
| Direct .NET method calls | Works | Blocked on non-whitelisted types | Stick to methods on String, Array, Hashtable, PSCustomObject |
Scripts written for FullLanguage often fail silently (empty output) or with cryptic PSSecurityException stack traces that don't identify the offending line. Test every module in CLM before deployment.
Design Change Triggers
- Need for .NET interop: Calling native APIs, using
System.DirectoryServices, or any non-whitelisted type forces a FullLanguage helper process. - Non-whitelisted cmdlets required: If your automation needs
Invoke-Sqlcmd,Get-ADUser, or vendor cmdlets, CLM cannot accommodate them. - PowerShell 7+ migration: PS7 expands the allowed type list (e.g.,
System.Collections.Generic.List`1) but on Linux/macOS, AppLocker/WDAC enforcement is absent — CLM relies on custom host policy or PSScriptAnalyzer rules. - Module compatibility: Microsoft-signed modules like Exchange Online Management or AzureAD often require FullLanguage and will not load in CLM.
Limitations You Must Accept
- Not a process boundary: Same process, same token. A host exploit bypasses CLM entirely.
- Policy misconfiguration defeats it: An AppLocker rule allowing unsigned scripts in FullLanguage nullifies CLM for those scripts.
- No network/file/environment restriction:
Get-Content,Set-Content,Invoke-WebRequest(if allowed), and$env:VARwork normally. CLM restricts capabilities, not resources. - PowerShell 7 on non-Windows: No AppLocker; you must enforce CLM via a custom host (e.g., a signed wrapper that sets
__PSLockdownPolicy) or accept that it's advisory only.
Practical Verification Checklist Before Deploy
- Deploy the AppLocker/WDAC policy to a test machine.
- Launch PowerShell under that policy:
powershell -Version 5.1 -Command "$ExecutionContext.SessionState.LanguageMode". - Run the four diagnostic commands above.
- Import each required module:
Import-Module MyModule -ErrorAction Stop; note which commands throw. - Execute representative scripts end-to-end; capture
PSSecurityExceptionoccurrences. - Document every FullLanguage helper you must spawn (e.g., a signed script that does
Add-Typeand returns JSON).
If step 4 or 5 reveals blocking exceptions you cannot rewrite around, CLM is the wrong boundary for that workload — use a separate account, Just Enough Administration (JEA), or a container instead.
When to Revisit the Design
Re-evaluate CLM when:
- PowerShell version upgrades (5.1 → 7.x) change the allowlist.
- New modules are added to the approved list.
- AppLocker/WDAC policy is updated — verify effective policy with
Get-AppLockerPolicy -Effective -Xmlquarterly. - Threat model shifts: if host compromise is in scope, CLM alone is insufficient.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.