From b87a5872ee64998119359791fc0c24cc5e39248a Mon Sep 17 00:00:00 2001 From: "Ray Huang (from Dev Box)" Date: Wed, 24 Jun 2026 10:21:57 +0800 Subject: [PATCH] fix: [MSRC] [123355] - UFO system - ufo/client/mcp/http_servers/linux_mcp_server.py and ufo/automator/app_apis/shell/shell_client.py - Allow-list bypass via interpreter -c/-Command --- tests/test_shell_client_path_validation.py | 35 ++++++++++++++++++++ ufo/automator/app_apis/shell/shell_client.py | 26 +++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/tests/test_shell_client_path_validation.py b/tests/test_shell_client_path_validation.py index e7d7a4e43..1b71cdb03 100644 --- a/tests/test_shell_client_path_validation.py +++ b/tests/test_shell_client_path_validation.py @@ -122,6 +122,28 @@ def test_newline_injection_blocked(self): sc._is_command_allowed("Get-ChildItem\nGet-Content secrets.txt") ) + def test_dotnet_method_invocation_blocked(self): + # CWE-184: an allow-listed cmdlet must not be usable as a springboard + # into arbitrary .NET static-method invocation carried inside plain + # parentheses / scriptblocks. + for cmd in ( + "Write-Output ([System.Diagnostics.Process]::Start('calc'))", + ( + "Get-ChildItem | ForEach-Object " + "{ [System.IO.File]::WriteAllText('poc.txt','proof') }" + ), + "Write-Output ([math]::Pi)", + "Write-Output (New-Object System.Net.WebClient)", + "Write-Output ([scriptblock]::Create('calc')).Invoke()", + "Get-ChildItem | & { calc }", + "Get-ChildItem | . { calc }", + ): + with self.subTest(cmd=cmd): + self.assertFalse( + sc._is_command_allowed(cmd), + f"{cmd!r} must be blocked by the denylist", + ) + def test_registry_and_provider_paths_blocked(self): for cmd in ( "Get-ChildItem HKLM:\\SOFTWARE", @@ -288,6 +310,19 @@ def test_blocks_env_expansion(self): "Get-Content $env:USERPROFILE\\.ssh\\id_rsa" ) + def test_blocks_dotnet_method_invocation(self): + # MSRC123355: .NET static-method invocation through an allow-listed + # cmdlet must be rejected before reaching ``powershell -Command``. + for cmd in ( + "Write-Output ([System.Diagnostics.Process]::Start('calc'))", + ( + "Get-ChildItem | ForEach-Object " + "{ [System.IO.File]::WriteAllText('poc.txt','proof') }" + ), + ): + with self.subTest(cmd=cmd): + self._assert_blocked(cmd) + if __name__ == "__main__": # pragma: no cover unittest.main() diff --git a/ufo/automator/app_apis/shell/shell_client.py b/ufo/automator/app_apis/shell/shell_client.py index 870f26913..a0d56d614 100644 --- a/ufo/automator/app_apis/shell/shell_client.py +++ b/ufo/automator/app_apis/shell/shell_client.py @@ -72,6 +72,19 @@ re.compile(r"\bAdd-Type\b", re.IGNORECASE), re.compile(r"\b(cmd|powershell|pwsh)(\.exe)?\s+[/-]", re.IGNORECASE), re.compile(r"[|;&`]\s*(bash|sh|cmd|powershell|pwsh)", re.IGNORECASE), + # .NET static-member / type-accelerator invocation, e.g. + # ``[System.Diagnostics.Process]::Start('calc')`` or + # ``[System.IO.File]::WriteAllText(...)``. PowerShell evaluates the + # ``-Command`` string as a full script, so a plain-parenthesised .NET + # call carried by an allow-listed cmdlet would otherwise reach arbitrary + # code execution (CWE-184). ``::`` has no legitimate use in the + # read-only cmdlet surface and is blocked outright. + re.compile(r"::"), + # Arbitrary object instantiation and dynamic method/scriptblock + # invocation that can be used to reach .NET primitives by other names. + re.compile(r"\bNew-Object\b", re.IGNORECASE), + re.compile(r"\.Invoke\b", re.IGNORECASE), + re.compile(r"[&.]\s*[({]", re.IGNORECASE), # call/dot-source operator re.compile(r"\bNew-Service\b|\bsc\.exe\b", re.IGNORECASE), re.compile(r"\breg(\.exe)?\s+(add|delete|import)", re.IGNORECASE), re.compile(r"\bschtasks(\.exe)?\b", re.IGNORECASE), @@ -371,6 +384,18 @@ def run_shell(self, params: Dict[str, Any]) -> Any: r"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" ) + # Force the PowerShell child process into ConstrainedLanguage mode. + # This is the primary runtime control: even if a crafted string slips + # past the allow-list / denylist, ConstrainedLanguage blocks arbitrary + # .NET type access and method invocation (e.g. + # ``[System.Diagnostics.Process]::Start`` or + # ``[System.IO.File]::WriteAllText``), which is the only way an + # allow-listed cmdlet can be escalated into code execution. The + # allow-list and dangerous-pattern denylist remain as + # defense-in-depth. + child_env = os.environ.copy() + child_env["__PSLockdownPolicy"] = "4" + try: # Use shell=False with an explicit argument list. # PowerShell's -NoProfile -NonInteractive flags prevent profile @@ -388,6 +413,7 @@ def run_shell(self, params: Dict[str, Any]) -> Any: shell=False, text=True, cwd=self.current_directory, + env=child_env, ) stdout, stderr = process.communicate(timeout=timeout)