如何防止 PowerShell 中的变量注入?

How can I prevent variable injection in PowerShell?

@Ansgar Wiechers 对最近的 PowerShell 问题的评论再次触发了我:DO NOT use Invoke-Expression 关于一个安全问题,我已经在脑海深处想了很久,需要问一下.

强声明(参考 Invoke-Expression considered harmful 文章)表明调用可以覆盖变量的脚本被认为是有害的。

还有PSScriptAnalyzer advises against using Invoke-Expression, see the AvoidUsingInvokeExpression rule.

但我自己曾经使用一种技术来更新递归脚本中的一个公共变量,它实际上可以覆盖其任何父作用域中的值,这很简单:

([Ref]$ParentVariable).Value = $NewValue

据我所知,潜在的恶意脚本也可以使用此技术在任何情况下注入变量,无论它是如何调用的...

考虑以下 "malicious" Inject.ps1 脚本:

([Ref]$MyValue).Value = 456
([Ref]$MyString).Value = 'Injected string'
([Ref]$MyObject).Value = [PSCustomObject]@{Name = 'Injected'; Value = 'Object'}

我的 Test.ps1 脚本:

$MyValue = 123
$MyString = "MyString"
$MyObject = [PSCustomObject]@{Name = 'My'; Value = 'Object'}
.\Inject.ps1
Write-Host $MyValue
Write-Host $MyString
Write-Host $MyObject

结果:

456
Injected string
@{Name=Injected; Value=Object}

如您所见,Test.ps1 范围内的所有三个变量都被 Inject.ps1 脚本覆盖。这也可以使用 Invoke-Command cmdlet 完成,我是否将变量的范围设置为 Private 甚至都没有关系:

New-Variable -Name MyValue -Value 123 -Scope Private
$MyString = "MyString"
$MyObject = [PSCustomObject]@{Name = 'My'; Value = 'Object'}
Invoke-Command {
    ([Ref]$MyValue).Value = 456
    ([Ref]$MyString).Value = 'Injected string'
    ([Ref]$MyObject).Value = [PSCustomObject]@{Name = 'Injected'; Value = 'Object'}
}
Write-Host $MyValue
Write-Host $MyString
Write-Host $MyObject

有没有办法将调用的 script/command 与覆盖当前作用域中的变量完全隔离?
如果不是,这是否可以被视为以任何方式调用脚本的安全风险?

反对使用 Invoke-Expression 的建议主要是为了防止意外 执行代码(代码注入)。

如果您调用一段 PowerShell 代码 - 无论是直接调用还是通过 Invoke-Expression - 它确实可以(可能是恶意地)操纵父作用域,包括全局作用域。
请注意,这种潜在的操作不限于 variables:例如,functionsaliases 可以是也修改了。

警告运行未知代码在两个方面有问题

  • 主要是为了有可能 直接 执行不需要的/破坏性的操作[1]
  • 其次,因为有可能恶意修改调用者的状态(变量,...),即只有方面下面的解决方案防范.

要提供所需的隔离,您有两个基本选择:

  • 运行 child 进程中的代码:

    • 通过启动另一个 PowerShell 实例;例如(在 Windows PowerShell 中使用 powershell 而不是 pwsh):

      • pwsh -c { ./someUntrustedScript.ps1 }
    • 通过启动后台作业;例如:

      • Start-Job { ./someUntrustedScript.ps1 } | Receive-Job -Wait -AutoRemove
  • 运行代码在一个单独的线程在同一个进程中:

    • 作为线程作业,通过Start-ThreadJob cmdlet(PowerShell [Core] 6+ 附带;在Windows PowerShell 中, 它可以安装 from the PowerShell Gallery 类似
      Install-Module -Scope CurrentUser ThreadJob);例如:

      • Start-ThreadJob { ./someUntrustedScript.ps1 } | Receive-Job -Wait -AutoRemove
    • 通过 PowerShell SDK 创建一个 new 运行space;例如:

      • [powershell]::Create().AddScript('./someUntrustedScript.ps1').Invoke()
      • 请注意,您必须做额外的工作才能获得输出流 other 而不是成功的输出流,特别是错误流的输出;此外,.Dispose() 应在命令完成后在 PowerShell 实例上调用。

基于子进程的解决方案会很慢,并且在您可以使用的数据类型方面受到限制return(由于涉及序列化/反序列化),但它提供对崩溃进程的调用代码的隔离。

thread-based 作业要快得多,可以 return 任何数据类型,但可能会导致整个过程崩溃。

在所有情况下,您都必须将调用代码需要访问的调用方的任何值作为 参数 传递,或者对于后台作业和线程作业,也可以通过 $using: 范围说明符。


js2010 提到了其他不太理想的替代方案:

  • Start-Process(子 process-based,带有 text-only 个参数和输出)

  • PowerShell Workflows,已过时(它们未移植到 PowerShell Core,以后也不会)。

  • Invoke-Command 与“环回远程处理”(-ComputerName localhost) 一起使用假设也是一种选择,但是您会招致子进程的双重开销和 HTTP-based 沟通;此外,您的计算机必须设置为远程处理,并且您必须 运行 提升权限(作为管理员)。


[1] 缓解问题的一种方法是限制允许哪些命令、语句、类型...在评估字符串时调用,这可以通过PowerShell SDK in combination with language modesand/or by explicitly constructing an initial session state. See 实现,以获取SDK使用语言模式的示例。