Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changes/v1.15/ENHANCEMENTS-20251022-162909.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
kind: ENHANCEMENTS
body: 'ssh-based provisioner (file + remote-exec): Re-enable support for PowerShell'
time: 2025-10-22T16:29:09.342697+01:00
custom:
Issue: "37794"
11 changes: 6 additions & 5 deletions internal/communicator/ssh/communicator.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,7 @@ func (c *Communicator) Upload(path string, input io.Reader) error {
return scpUploadFile(targetFile, input, w, stdoutR, size)
}

cmd, err := quoteShell([]string{"scp", "-vt", targetDir}, c.connInfo.TargetPlatform)
cmd, err := quoteScpCommand([]string{"scp", "-vt", targetDir}, c.connInfo.TargetPlatform)
if err != nil {
return err
}
Expand Down Expand Up @@ -509,7 +509,7 @@ func (c *Communicator) UploadDir(dst string, src string) error {
return uploadEntries()
}

cmd, err := quoteShell([]string{"scp", "-rvt", dst}, c.connInfo.TargetPlatform)
cmd, err := quoteScpCommand([]string{"scp", "-rvt", dst}, c.connInfo.TargetPlatform)
if err != nil {
return err
}
Expand Down Expand Up @@ -886,14 +886,15 @@ func (c *bastionConn) Close() error {
return c.Bastion.Close()
}

func quoteShell(args []string, targetPlatform string) (string, error) {
func quoteScpCommand(args []string, targetPlatform string) (string, error) {
if targetPlatform == TargetPlatformUnix {
return shquot.POSIXShell(args), nil
}
if targetPlatform == TargetPlatformWindows {
return shquot.WindowsArgv(args), nil
cmd, args := shquot.WindowsArgvSplit(args)
return fmt.Sprintf("%s %s", cmd, args), nil
}

return "", fmt.Errorf("Cannot quote shell command, target platform unknown: %s", targetPlatform)
return "", fmt.Errorf("Cannot quote scp command, target platform unknown: %s", targetPlatform)

}
143 changes: 142 additions & 1 deletion internal/communicator/ssh/communicator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/hashicorp/terraform/internal/communicator/remote"
"github.com/zclconf/go-cty/cty"
"golang.org/x/crypto/ssh"
Expand Down Expand Up @@ -660,7 +661,7 @@ func TestAccHugeUploadFile(t *testing.T) {
return scpUploadFile(targetFile, source, w, stdoutR, size)
}

cmd, err := quoteShell([]string{"scp", "-vt", targetDir}, c.connInfo.TargetPlatform)
cmd, err := quoteScpCommand([]string{"scp", "-vt", targetDir}, c.connInfo.TargetPlatform)
if err != nil {
t.Fatal(err)
}
Expand All @@ -680,6 +681,146 @@ func TestAccHugeUploadFile(t *testing.T) {
}
}

func TestQuoteScpCommand(t *testing.T) {
testCases := []struct {
inputArgs []string
platform string
expectedCmd string
}{
// valid Unix command
{
[]string{"scp", "-vt", "/var/path"},
TargetPlatformUnix,
"'scp' -vt /var/path",
},

// command injection attempt in Unix
{
[]string{"scp", "-vt", "/var/path;rm"},
TargetPlatformUnix,
"'scp' -vt /var/path\\;rm",
},
{
[]string{"scp", "-vt", "/var/path&&rm"},
TargetPlatformUnix,
"'scp' -vt /var/path\\&\\&rm",
},
{
[]string{"scp", "-vt", "/var/path|rm"},
TargetPlatformUnix,
"'scp' -vt /var/path\\|rm",
},
{
[]string{"scp", "-vt", "/var/path||rm"},
TargetPlatformUnix,
"'scp' -vt /var/path\\|\\|rm",
},
{
[]string{"scp", "-vt", "/var/path; rm"},
TargetPlatformUnix,
"'scp' -vt '/var/path; rm'",
},
{
[]string{"scp", "-vt", "/var/path`rm`"},
TargetPlatformUnix,
"'scp' -vt /var/path\\`rm\\`",
},
{
[]string{"scp", "-vt", "/var/path$(rm)"},
TargetPlatformUnix,
"'scp' -vt /var/path\\$\\(rm\\)",
},

// valid Windows commands
{
[]string{"scp", "-vt", "C:\\Windows\\Temp"},
TargetPlatformWindows,
"scp -vt C:\\Windows\\Temp",
},
{
[]string{"scp", "-vt", "C:\\Windows\\Temp With Space"},
TargetPlatformWindows,
"scp -vt \"C:\\Windows\\Temp With Space\"",
},

// command injection attempt in Windows
{
[]string{"scp", "-vt", "C:\\Windows\\Temp ;rmdir"},
TargetPlatformWindows,
"scp -vt \"C:\\Windows\\Temp ;rmdir\"",
},
{
[]string{"scp", "-vt", "C:\\Windows\\Temp\";rmdir"},
TargetPlatformWindows,
"scp -vt \"C:\\Windows\\Temp\\\";rmdir\"",
},
{
[]string{"scp", "-vt", "C:\\Windows\\Temp\nrmdir"},
TargetPlatformWindows,
"scp -vt \"C:\\Windows\\Temp\nrmdir\"",
},
{
[]string{"scp", "-vt", "C:\\Windows\\Temp\trmdir"},
TargetPlatformWindows,
"scp -vt \"C:\\Windows\\Temp\trmdir\"",
},
{
[]string{"scp", "-vt", "C:\\Windows\\Temp\vrmdir"},
TargetPlatformWindows,
"scp -vt \"C:\\Windows\\Temp\vrmdir\"",
},
{
[]string{"scp", "-vt", "C:\\Windows\\Temp\u0020rmdir"},
TargetPlatformWindows,
"scp -vt \"C:\\Windows\\Temp rmdir\"",
},

// There is no special handling of the injection attempts below
// but we include them anyway to demonstrate this
// and to avoid any regressions due to upstream changes.
Comment on lines +778 to +780
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding based on reading the upstream library and also manual testing in a Windows VM is that Windows does not let you break out of the command or at least not in the context of how we execute scp as the shell does not seem to interpret the individual arguments directly. That is why escaping of special characters on Windows does not need to be as rigorous in this particular context.

I'm happy to be corrected here. Either way the PR isn't introducing anything new here - it's only changing how the command name gets treated, all arguments are treated exactly the same as before.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. I don't have much experience with Powershell, so I'd defer to you judgement here.

{
[]string{"scp", "-vt", "C:\\Windows\\Temp;rmdir"},
TargetPlatformWindows,
"scp -vt C:\\Windows\\Temp;rmdir",
},
{
[]string{"scp", "-vt", "C:\\Windows\\Temp&rmdir"},
TargetPlatformWindows,
"scp -vt C:\\Windows\\Temp&rmdir",
},
{
[]string{"scp", "-vt", "C:\\Windows\\Temp&&rmdir"},
TargetPlatformWindows,
"scp -vt C:\\Windows\\Temp&&rmdir",
},
{
[]string{"scp", "-vt", "C:\\Windows\\Temp|rmdir"},
TargetPlatformWindows,
"scp -vt C:\\Windows\\Temp|rmdir",
},
{
[]string{"scp", "-vt", "C:\\Windows\\Temp||rmdir"},
TargetPlatformWindows,
"scp -vt C:\\Windows\\Temp||rmdir",
},
{
[]string{"scp", "-vt", "C:\\Windows\\Temp$(rmdir)"},
TargetPlatformWindows,
"scp -vt C:\\Windows\\Temp$(rmdir)",
},
}

for _, tc := range testCases {
cmd, err := quoteScpCommand(tc.inputArgs, tc.platform)
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(tc.expectedCmd, cmd); diff != "" {
t.Fatalf("unexpected command for %q: %s", tc.inputArgs, diff)
}
}
}

func TestScriptPath(t *testing.T) {
cases := []struct {
Input string
Expand Down