fix(agent/agentcontainers): prevent command injection in shell execer (#26235)

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.
This commit is contained in:
Zach
2026-06-11 14:56:38 -04:00
committed by GitHub
parent fda6fc9345
commit 112c921235
3 changed files with 99 additions and 20 deletions
+7 -9
View File
@@ -3923,16 +3923,14 @@ func TestAPI(t *testing.T) {
// Verify commands were executed through the custom shell and environment.
require.NotEmpty(t, fakeExec.commands, "commands should be executed")
// Want: /bin/custom-shell -c '"docker" "ps" "--all" "--quiet" "--no-trunc"'
// Want: /bin/custom-shell -c "$@" "" docker ps --all --quiet --no-trunc
// The command is passed as positional parameters and run via "$@" so
// the shell forwards argv without re-parsing it.
require.Equal(t, testShell, fakeExec.commands[0][0], "custom shell should be used")
if runtime.GOOS == "windows" {
require.Equal(t, "/c", fakeExec.commands[0][1], "shell should be called with /c on Windows")
} else {
require.Equal(t, "-c", fakeExec.commands[0][1], "shell should be called with -c")
}
require.Len(t, fakeExec.commands[0], 3, "command should have 3 arguments")
require.GreaterOrEqual(t, strings.Count(fakeExec.commands[0][2], " "), 2, "command/script should have multiple arguments")
require.True(t, strings.HasPrefix(fakeExec.commands[0][2], `"docker" "ps"`), "command should start with \"docker\" \"ps\"")
require.Equal(t, "-c", fakeExec.commands[0][1], "shell should be called with -c")
require.Equal(t, `"$@"`, fakeExec.commands[0][2], "script should run argv via \"$@\"")
require.Equal(t, "", fakeExec.commands[0][3], "$0 slot should be an empty placeholder")
require.Equal(t, []string{"docker", "ps", "--all", "--quiet", "--no-trunc"}, fakeExec.commands[0][4:], "argv should be passed through unquoted")
// Verify the environment was set on the command.
lastCmd := fakeExec.getLastCommand()