As long as the string contains a valid expression, you can use the [scriptblock]::Create(..)
method:
$String = '@("a value","b value","c value")'
& ([scriptblock]::Create($String))
Invoke-Expression
would also work and in this case would be mostly the same thing.
However, the nice feature about Script Blocks, as zett42 pointed out in a comment, is that with them we can validate that arbitrary code execution is forbidden with it's CheckRestrictedLanguage
method.
In example below, Write-Host
is an allowed command and a string containing only 'Write-Host "Hello world!"'
would not throw an exception however, assignment statements or any other command not listed in $allowedCommands
will throw and the script block will not be executed.
$String = @'
Write-Host "Hello world!"
$stream = [System.Net.Sockets.TcpClient]::new('google.com', 80).GetStream()
'@
[string[]] $allowedCommands = 'Write-Host'
[string[]] $allowedVaribales = ''
try {
$scriptblock = [scriptblock]::Create($String)
$scriptblock.CheckRestrictedLanguage(
$allowedCommands,
$allowedVaribales,
$false # Argument to allow Environmental Variables
)
& $scriptblock
}
catch {
Write-Warning $_.Exception.Message
}
Another alternative is to run the expression in a Runspace with ConstrainedLanguage
Mode. This function can make it really easy.
using namespace System.Management.Automation.Language
using namespace System.Collections.Generic
using namespace System.Management.Automation.Runspaces
function Invoke-ConstrainedExpression {
[CmdletBinding(DefaultParameterSetName = 'ScriptBlock')]
param(
[Parameter(ParameterSetName = 'Command', Mandatory, ValueFromPipeline, Position = 0)]
[string] $Command,
[Parameter(ParameterSetName = 'ScriptBlock', Mandatory, Position = 0)]
[scriptblock] $ScriptBlock,
[Parameter()]
[Management.Automation.PSLanguageMode] $LanguageMode = 'ConstrainedLanguage',
# When using this switch, the function inspects the AST to find any variable
# not being an assigned one in the expression, queries the local state to find
# it's value and injects that variable to the Initial Session State of the Runspace.
[Parameter()]
[switch] $InjectLocalVariables
)
process {
try {
$Expression = $ScriptBlock
if($PSBoundParameters.ContainsKey('Command')) {
$Expression = [scriptblock]::Create($Command)
}
# bare minimum for the session state
$iss = [initialsessionstate]::CreateDefault2()
# set `ContrainedLanguage` for this session
$iss.LanguageMode = $LanguageMode
if($InjectLocalVariables.IsPresent) {
$ast = $Expression.Ast
$map = [HashSet[string]]::new([StringComparer]::InvariantCultureIgnoreCase)
$ast.FindAll({ $args[0] -is [AssignmentStatementAst] }, $true).Left.Extent.Text |
ForEach-Object { $null = $map.Add($_) }
$variablesToInject = $ast.FindAll({
$args[0] -is [VariableExpressionAst] -and
-not $map.Contains($args[0].Extent.Text)
}, $true).VariablePath.UserPath
foreach($var in $variablesToInject) {
$value = $ExecutionContext.SessionState.PSVariable.GetValue($var)
$entry = [SessionStateVariableEntry]::new($var, $value, '')
$iss.Variables.Add($entry)
}
}
# create the PS Instance and add the expression to invoke
$ps = [powershell]::Create($iss).AddScript($Expression)
# invoke the expression
[Collections.Generic.List[object]] $stdout = $ps.Invoke()
$streams = $ps.Streams
$streams.PSObject.Properties.Add([psnoteproperty]::new('Success', $stdout))
$streams
}
finally {
if($ps) { $ps.Dispose() }
}
}
}
Now we can test the expression using Constrained Language:
Invoke-ConstrainedExpression {
Write-Host 'Starting script'
[System.Net.WebClient]::new().DownloadString($uri) | iex
'Hello world!'
}
The output would look like this:
Success : {Hello world!}
Error : {Cannot create type. Only core types are supported in this language mode.}
Progress : {}
Verbose : {}
Debug : {}
Warning : {}
Information : {Starting script}
DevBlogs Article: PowerShell Constrained Language Mode has some nice information and is definitely worth a read.