Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/System.Management.Automation/engine/ExecutionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,18 @@ internal PSLanguageMode LanguageMode
/// </summary>
internal bool HasRunspaceEverUsedConstrainedLanguageMode { get; private set; }

/// <summary>
/// True when this runspace's <see cref="PSLanguageMode.ConstrainedLanguage"/> mode was applied solely
/// because the system-wide App Control (WDAC) policy is in audit mode.
/// In audit mode no restrictions are enforced; only audit events are emitted.
/// This is distinct from a runspace that is explicitly configured for ConstrainedLanguage
/// (for example, via a session/JEA configuration or an enforced system lockdown policy),
/// whose security boundary must be preserved.
/// The value is set only by <see cref="Utils.EnforceSystemLockDownLanguageMode"/> when it changes a
/// FullLanguage runspace to ConstrainedLanguage as a result of an audit-mode policy.
/// </summary>
internal bool LanguageModeWasSetByAppControlAudit { get; set; }

/// <summary>
/// Indicate if a parameter binding is happening that transitions the execution from ConstrainedLanguage
/// mode to a trusted FullLanguage command.
Expand Down
6 changes: 6 additions & 0 deletions src/System.Management.Automation/engine/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1508,6 +1508,12 @@ internal static PSLanguageMode EnforceSystemLockDownLanguageMode(ExecutionContex
// Set to ConstrainedLanguage mode. But no restrictions are applied in audit mode
// and only audit messages will be emitted to logs.
context.LanguageMode = PSLanguageMode.ConstrainedLanguage;

// Record that this ConstrainedLanguage state originates from the audit-mode policy
// (and not from an explicit session/JEA configuration or an enforced lockdown).
// Consumers such as Start-Job use this to distinguish an audit-induced boundary,
// which imposes no restrictions, from an independently configured one.
context.LanguageModeWasSetByAppControlAudit = true;
break;
}
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -644,11 +644,16 @@ protected override void BeginProcessing()
/// </summary>
protected override void CreateHelpersForSpecifiedComputerNames()
{
// If we're in ConstrainedLanguage mode and the system is in lockdown mode,
// ensure that they haven't specified a ScriptBlock or InitScript - as
// we can't protect that boundary.
// If we're in ConstrainedLanguage mode and the system is not in enforced lockdown,
// ensure that they haven't specified a ScriptBlock or InitScript, because the child process
// could otherwise run in FullLanguage and bypass the ConstrainedLanguage boundary.
// Exception: when ConstrainedLanguage was applied solely because the system-wide App Control (WDAC)
// policy is in audit mode (log-only) and the child process independently re-evaluates the policy.
// An explicitly configured ConstrainedLanguage runspace (for example, via a session/JEA configuration)
// does not set this flag and remains blocked.
if ((Context.LanguageMode == PSLanguageMode.ConstrainedLanguage) &&
(SystemPolicy.GetSystemLockdownPolicy() != SystemEnforcementMode.Enforce) &&
!Context.LanguageModeWasSetByAppControlAudit &&
((ScriptBlock != null) || (InitializationScript != null)))
Comment thread
jagilber marked this conversation as resolved.
{
ThrowTerminatingError(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,95 @@ try
$expectedError.FullyQualifiedErrorId | Should -BeExactly "CannotStartJobInconsistentLanguageMode,Microsoft.PowerShell.Commands.StartJobCommand"
}
}

Context "Background jobs in App Control audit mode" {

It "Verifies that Start-Job succeeds when ConstrainedLanguage was applied by an audit-mode policy" {

try
{
# 0x80000008 = WLDP_LOCKDOWN_DEFINED_FLAG (0x80000000) | WLDP_LOCKDOWN_UMCIAUDIT_FLAG (8)
# which the debug lockdown policy hook maps to SystemEnforcementMode.Audit.
# Set the system-wide policy to audit mode before starting a fresh pwsh process so that
# its start-up path applies audit-induced ConstrainedLanguage (issue #25109).
[System.Environment]::SetEnvironmentVariable("__PSLockdownPolicy", "0x80000008", [System.EnvironmentVariableTarget]::Machine)

$command = @'
$languageMode = $ExecutionContext.SessionState.LanguageMode
$job = Start-Job -ScriptBlock { "AuditJobResult" } | Wait-Job
$output = Receive-Job -Job $job
$job | Remove-Job
"LanguageMode=$languageMode;Output=$output"
'@
$result = pwsh.exe -noprofile -nologo -c $command
}
finally
{
[System.Environment]::SetEnvironmentVariable("__PSLockdownPolicy", $null, [System.EnvironmentVariableTarget]::Machine)
}

# The child session must be in ConstrainedLanguage (audit-induced) and the job must run.
$result | Should -BeLike "*LanguageMode=ConstrainedLanguage*"
$result | Should -BeLike "*Output=AuditJobResult*"
}
}

Context "Background jobs with explicit constrained language under App Control audit mode" {

It "Verifies that Start-Job remains denied for an explicitly constrained session even when audit mode is active" {

try
{
# Audit mode is active system-wide (0x80000008 => SystemEnforcementMode.Audit), but this session
# is explicitly constrained (for example, a session/JEA configuration). That security boundary
# must be preserved and Start-Job must fail.
[System.Environment]::SetEnvironmentVariable("__PSLockdownPolicy", "0x80000008", [System.EnvironmentVariableTarget]::Machine)
$ExecutionContext.SessionState.LanguageMode = "ConstrainedLanguage"
Start-Job { [object]::Equals("A", "B") }
throw "No Exception!"
}
catch
{
$expectedError = $_
}
finally
{
Invoke-LanguageModeTestingSupportCmdlet -EnableFullLanguageMode
[System.Environment]::SetEnvironmentVariable("__PSLockdownPolicy", $null, [System.EnvironmentVariableTarget]::Machine)
}

$expectedError.FullyQualifiedErrorId | Should -BeExactly "CannotStartJobInconsistentLanguageMode,Microsoft.PowerShell.Commands.StartJobCommand"
}
}

Context "Background jobs in a configured constrained-language runspace under App Control audit mode" {

It "Verifies that Start-Job remains denied in an InitialSessionState-configured ConstrainedLanguage runspace even under audit mode" {

try
{
# Audit mode is active system-wide (0x80000008 => SystemEnforcementMode.Audit). A runspace
# explicitly configured for ConstrainedLanguage via InitialSessionState (the mechanism used by
# session/JEA configurations) is NOT audit-induced, so its security boundary must be preserved
# and Start-Job must fail - even though an audit-induced session in the same process is allowed.
[System.Environment]::SetEnvironmentVariable("__PSLockdownPolicy", "0x80000008", [System.EnvironmentVariableTarget]::Machine)

$iss = [initialsessionstate]::CreateDefault2()
$iss.LanguageMode = "ConstrainedLanguage"
$rs = [runspacefactory]::CreateRunspace($iss)
$rs.Open()
$pl = $rs.CreatePipeline('try { Start-Job -ScriptBlock { 1 } | Out-Null; "NoError" } catch { $_.FullyQualifiedErrorId }')
$result = $pl.Invoke()
$rs.Dispose()
}
finally
{
[System.Environment]::SetEnvironmentVariable("__PSLockdownPolicy", $null, [System.EnvironmentVariableTarget]::Machine)
}

$result[0] | Should -BeExactly "CannotStartJobInconsistentLanguageMode,Microsoft.PowerShell.Commands.StartJobCommand"
}
}
}

Describe "Add-Type in constrained language" -Tags 'Feature','RequireAdminOnWindows' {
Expand Down