mirror of
https://github.com/coder/coder.git
synced 2026-09-23 22:20:22 +08:00
commandEnvExecer.prepare rebuilt commands into a single shell string
using `fmt.Sprintf("%q", arg)`, which produces Go string literals, not
shell-quoted tokens. Go's %q does not escape `$`, backticks, or other
metacharacters that remain active inside double quotes, so an argument
such as `$(...)` was evaluated by the shell as command substitution.
Arguments flow from devcontainer config and workspace-folder, making
this exploitable.
Pass the command to the shell as positional parameters and run `"$@"` so
the shell forwards argv verbatim without re-parsing it.
The Windows previous handling is not required because Coder doesn't
support devcontainers on Windows, so it is removed.
85 lines
2.4 KiB
Go
85 lines
2.4 KiB
Go
package agentcontainers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"path/filepath"
|
|
"runtime"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"cdr.dev/slog/v3"
|
|
"cdr.dev/slog/v3/sloggers/slogtest"
|
|
"github.com/coder/coder/v2/agent/agentexec"
|
|
"github.com/coder/coder/v2/agent/usershell"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
func TestCommandEnvExecer_Prepare(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("the POSIX shell quoting under test does not apply on Windows")
|
|
}
|
|
|
|
const shell = "/bin/sh"
|
|
commandEnv := func(usershell.EnvInfoer, []string) (string, string, []string, error) {
|
|
return shell, "/tmp", []string{"FOO=bar"}, nil
|
|
}
|
|
e := newCommandEnvExecer(slogtest.Make(t, nil).Leveled(slog.LevelDebug), commandEnv, agentexec.DefaultExecer)
|
|
|
|
t.Run("ArgvPassthrough", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
name, args, dir, env := e.prepare(context.Background(), "echo", "hello", "world")
|
|
// The command is run as: shell -c "$@" "" <argv...> so that the
|
|
// shell re-emits argv without re-parsing it. The empty $0 slot is
|
|
// discarded.
|
|
require.Equal(t, shell, name)
|
|
require.Equal(t, []string{"-c", `"$@"`, "", "echo", "hello", "world"}, args)
|
|
require.Equal(t, "/tmp", dir)
|
|
require.Equal(t, []string{"FOO=bar"}, env)
|
|
})
|
|
|
|
t.Run("MetacharactersNotInterpreted", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
payloads := []string{
|
|
"$(echo INJECTED)",
|
|
"`echo INJECTED`",
|
|
"$HOME",
|
|
"a; echo INJECTED",
|
|
"a && echo INJECTED",
|
|
"a | echo INJECTED",
|
|
"a\necho INJECTED",
|
|
"it's a \"test\" \\ end",
|
|
"",
|
|
}
|
|
for _, payload := range payloads {
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
cmd := e.CommandContext(ctx, "printf", "%s", payload)
|
|
var out bytes.Buffer
|
|
cmd.Stdout = &out
|
|
cmd.Stderr = &out
|
|
require.NoError(t, cmd.Run(), "payload %q", payload)
|
|
assert.Equal(t, payload, out.String(), "payload %q was altered by the shell", payload)
|
|
}
|
|
})
|
|
|
|
t.Run("CommandSubstitutionHasNoSideEffect", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
marker := filepath.Join(t.TempDir(), "pwned")
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
cmd := e.CommandContext(ctx, "echo", "$(touch "+marker+")")
|
|
var out bytes.Buffer
|
|
cmd.Stdout = &out
|
|
cmd.Stderr = &out
|
|
require.NoError(t, cmd.Run())
|
|
require.Equal(t, "$(touch "+marker+")\n", out.String())
|
|
require.NoFileExists(t, marker, "command substitution executed; injection is possible")
|
|
})
|
|
}
|