◐ Shell
clean mode source ↗

fix(agent/agentcontainers): prevent command injection in shell execer… · coder/coder@94ee8fb

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+

}