Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit 460c53f

Browse files
authored
fix: Escape shell arguments (#227)
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.
1 parent 51fd2d3 commit 460c53f

File tree

2 files changed

+74
-1
lines changed

2 files changed

+74
-1
lines changed

internal/cmd/shell.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,46 @@ coder sh front-end-dev cat ~/config.json`,
8181
}
8282
}
8383

84+
// shellEscape escapes an argument so that we can pass it 'sh -c'
85+
// and have it do the right thing.
86+
//
87+
// Use this to ensure that the result of a command running in
88+
// the development environment behaves the same as the command
89+
// running via "coder sh".
90+
//
91+
// For example:
92+
//
93+
// $ coder sh env
94+
// $ go run ~/test.go 1 2 "3 4" '"abc def" \\abc' 5 6 "7 8 9"
95+
//
96+
// should produce the same output as:
97+
//
98+
// $ coder sh go run ~/test.go 1 2 "3 4" '"abc def" \\abc' 5 6 "7 8 9"
99+
func shellEscape(arg string) string {
100+
r := strings.NewReplacer(`\`, `\\`, `"`, `\"`, `'`, `\'`, ` `, `\ `)
101+
return r.Replace(arg)
102+
}
103+
84104
func shell(cmd *cobra.Command, cmdArgs []string) error {
85105
ctx := cmd.Context()
106+
86107
var command string
87108
var args []string
88109
if len(cmdArgs) > 1 {
110+
var escapedArgs strings.Builder
111+
112+
for i, arg := range cmdArgs[1:] {
113+
escapedArgs.WriteString(shellEscape(arg))
114+
115+
// Add spaces between arguments, except the last argument
116+
if i < len(cmdArgs)-2 {
117+
escapedArgs.WriteByte(' ')
118+
}
119+
}
120+
89121
command = "/bin/sh"
90122
args = []string{"-c"}
91-
args = append(args, strings.Join(cmdArgs[1:], " "))
123+
args = append(args, escapedArgs.String())
92124
} else {
93125
// Bring user into shell if no command is specified.
94126
shell := "$(getent passwd $(id -u) | cut -d: -f 7)"

internal/cmd/shell_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package cmd
2+
3+
import "testing"
4+
5+
func TestShellEscape(t *testing.T) {
6+
t.Parallel()
7+
8+
tests := []struct {
9+
Name string
10+
Input string
11+
Escaped string
12+
}{
13+
{
14+
Name: "single space",
15+
Input: "hello world",
16+
Escaped: `hello\ world`,
17+
},
18+
{
19+
Name: "multiple spaces",
20+
Input: "test message hello world",
21+
Escaped: `test\ message\ hello\ \ world`,
22+
},
23+
{
24+
Name: "mixed quotes",
25+
Input: `"''"`,
26+
Escaped: `\"\'\'\"`,
27+
},
28+
{
29+
Name: "mixed escaped quotes",
30+
Input: `"'\"\"'"`,
31+
Escaped: `\"\'\\\"\\\"\'\"`,
32+
},
33+
}
34+
35+
for _, test := range tests {
36+
if e, a := test.Escaped, shellEscape(test.Input); e != a {
37+
t.Fatalf("test %q failed; expected: %q, got %q (input: %q)",
38+
test.Name, test.Escaped, a, test.Input)
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)