fix(agent/agentcontainers): prevent command injection in shell execer… · coder/coder@b949480
1+package agentcontainers
2+3+import (
4+"bytes"
5+"context"
6+"path/filepath"
7+"runtime"
8+"testing"
9+10+"github.com/stretchr/testify/assert"
11+"github.com/stretchr/testify/require"
12+13+"cdr.dev/slog/v3"
14+"cdr.dev/slog/v3/sloggers/slogtest"
15+"github.com/coder/coder/v2/agent/agentexec"
16+"github.com/coder/coder/v2/agent/usershell"
17+"github.com/coder/coder/v2/testutil"
18+)
19+20+func TestCommandEnvExecer_Prepare(t *testing.T) {
21+t.Parallel()
22+23+if runtime.GOOS == "windows" {
24+t.Skip("the POSIX shell quoting under test does not apply on Windows")
25+ }
26+27+const shell = "/bin/sh"
28+commandEnv := func(usershell.EnvInfoer, []string) (string, string, []string, error) {
29+return shell, "/tmp", []string{"FOO=bar"}, nil
30+ }
31+e := newCommandEnvExecer(slogtest.Make(t, nil).Leveled(slog.LevelDebug), commandEnv, agentexec.DefaultExecer)
32+33+t.Run("ArgvPassthrough", func(t *testing.T) {
34+t.Parallel()
35+36+name, args, dir, env := e.prepare(context.Background(), "echo", "hello", "world")
37+// The command is run as: shell -c "$@" "" <argv...> so that the
38+// shell re-emits argv without re-parsing it. The empty $0 slot is
39+// discarded.
40+require.Equal(t, shell, name)
41+require.Equal(t, []string{"-c", `"$@"`, "", "echo", "hello", "world"}, args)
42+require.Equal(t, "/tmp", dir)
43+require.Equal(t, []string{"FOO=bar"}, env)
44+ })
45+46+t.Run("MetacharactersNotInterpreted", func(t *testing.T) {
47+t.Parallel()
48+49+payloads := []string{
50+"$(echo INJECTED)",
51+"`echo INJECTED`",
52+"$HOME",
53+"a; echo INJECTED",
54+"a && echo INJECTED",
55+"a | echo INJECTED",
56+"a\necho INJECTED",
57+"it's a \"test\" \\ end",
58+"",
59+ }
60+for _, payload := range payloads {
61+ctx := testutil.Context(t, testutil.WaitShort)
62+cmd := e.CommandContext(ctx, "printf", "%s", payload)
63+var out bytes.Buffer
64+cmd.Stdout = &out
65+cmd.Stderr = &out
66+require.NoError(t, cmd.Run(), "payload %q", payload)
67+assert.Equal(t, payload, out.String(), "payload %q was altered by the shell", payload)
68+ }
69+ })
70+71+t.Run("CommandSubstitutionHasNoSideEffect", func(t *testing.T) {
72+t.Parallel()
73+74+marker := filepath.Join(t.TempDir(), "pwned")
75+ctx := testutil.Context(t, testutil.WaitShort)
76+cmd := e.CommandContext(ctx, "echo", "$(touch "+marker+")")
77+var out bytes.Buffer
78+cmd.Stdout = &out
79+cmd.Stderr = &out
80+require.NoError(t, cmd.Run())
81+require.Equal(t, "$(touch "+marker+")\n", out.String())
82+require.NoFileExists(t, marker, "command substitution executed; injection is possible")
83+ })
84+}