From 0df7ec521444b436e7ba0a61ff9598b3ac71920f Mon Sep 17 00:00:00 2001 From: Jonathan Yu Date: Sun, 24 Jan 2021 01:52:57 +0000 Subject: [PATCH] fix: Escape shell arguments The commands passed to "coder sh" are passed as a single argument to "sh -c", so we need to shell-escape the command we pass. This change escapes spaces, backslash, and quotes. --- internal/cmd/shell.go | 34 ++++++++++++++++++++++++++++++- internal/cmd/shell_test.go | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 internal/cmd/shell_test.go diff --git a/internal/cmd/shell.go b/internal/cmd/shell.go index 5dd5389d..e3a2ba2e 100644 --- a/internal/cmd/shell.go +++ b/internal/cmd/shell.go @@ -81,14 +81,46 @@ coder sh front-end-dev cat ~/config.json`, } } +// shellEscape escapes an argument so that we can pass it 'sh -c' +// and have it do the right thing. +// +// Use this to ensure that the result of a command running in +// the development environment behaves the same as the command +// running via "coder sh". +// +// For example: +// +// $ coder sh env +// $ go run ~/test.go 1 2 "3 4" '"abc def" \\abc' 5 6 "7 8 9" +// +// should produce the same output as: +// +// $ coder sh go run ~/test.go 1 2 "3 4" '"abc def" \\abc' 5 6 "7 8 9" +func shellEscape(arg string) string { + r := strings.NewReplacer(`\`, `\\`, `"`, `\"`, `'`, `\'`, ` `, `\ `) + return r.Replace(arg) +} + func shell(cmd *cobra.Command, cmdArgs []string) error { ctx := cmd.Context() + var command string var args []string if len(cmdArgs) > 1 { + var escapedArgs strings.Builder + + for i, arg := range cmdArgs[1:] { + escapedArgs.WriteString(shellEscape(arg)) + + // Add spaces between arguments, except the last argument + if i < len(cmdArgs)-2 { + escapedArgs.WriteByte(' ') + } + } + command = "/bin/sh" args = []string{"-c"} - args = append(args, strings.Join(cmdArgs[1:], " ")) + args = append(args, escapedArgs.String()) } else { // Bring user into shell if no command is specified. shell := "$(getent passwd $(id -u) | cut -d: -f 7)" diff --git a/internal/cmd/shell_test.go b/internal/cmd/shell_test.go new file mode 100644 index 00000000..9c5875b5 --- /dev/null +++ b/internal/cmd/shell_test.go @@ -0,0 +1,41 @@ +package cmd + +import "testing" + +func TestShellEscape(t *testing.T) { + t.Parallel() + + tests := []struct { + Name string + Input string + Escaped string + }{ + { + Name: "single space", + Input: "hello world", + Escaped: `hello\ world`, + }, + { + Name: "multiple spaces", + Input: "test message hello world", + Escaped: `test\ message\ hello\ \ world`, + }, + { + Name: "mixed quotes", + Input: `"''"`, + Escaped: `\"\'\'\"`, + }, + { + Name: "mixed escaped quotes", + Input: `"'\"\"'"`, + Escaped: `\"\'\\\"\\\"\'\"`, + }, + } + + for _, test := range tests { + if e, a := test.Escaped, shellEscape(test.Input); e != a { + t.Fatalf("test %q failed; expected: %q, got %q (input: %q)", + test.Name, test.Escaped, a, test.Input) + } + } +}