fix(agent/agentcontainers): prevent command injection in shell execer by zedkipp · Pull Request #26235 · coder/coder · GitHub
Skip to content

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

Merged
f0ssel merged 2 commits into
mainfrom
zedkipp/plat-261-docker-interoplation
Jun 11, 2026
Merged

fix(agent/agentcontainers): prevent command injection in shell execer#26235
f0ssel merged 2 commits into
mainfrom
zedkipp/plat-261-docker-interoplation

Conversation

@zedkipp

@zedkipp zedkipp commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

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.

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 path
keeps its prior behavior. It's suspected to already be non-functional
(the selected shell is usually PowerShell, which rejects the hardcoded
cmd.exe "/c" switch) and a correct implementation is deferred.
@linear-code

linear-code Bot commented Jun 10, 2026

Copy link
Copy Markdown

Comment thread agent/agentcontainers/execer.go Outdated
args = append(args, fmt.Sprintf("%q", arg))
cmdArgs := append([]string{inName}, inArgs...)
if runtime.GOOS == "windows" {
// Preserve the prior quoting behavior on Windows. This path was

@zedkipp zedkipp Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly I don't understand how/if this worked on Windows before, nor why there is any Windows handling here. usershell.get() prefers Powershell, but we use "/c" which seems to target cmd.exe. %q doesn't seem like the correct quoting model for either shell.

In any case, the fix addresses the core problem on Linux which is what we officially support for dev containers (https://coder.com/docs/user-guides/devcontainers#limitations). I want to delete the windows cases, but I've opted to leave them as-is for now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mafredri do you have any context here that would helpful?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My best guess is this was about leaving the door open to expand support to using Dev Containers on not just a Linux-based host workspace, but also Windows workspaces given the presence of Docker.

But unless there's motivation to actually do that it's just dead code and we should remove it and disable the tests for Windows.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I decided to remove the Windows handling here since I don't have any reason to keep it around and it doesn't seem correct anyway.

@zedkipp zedkipp requested a review from mafredri June 10, 2026 22:14
@zedkipp zedkipp marked this pull request as ready for review June 10, 2026 22:16
Comment thread agent/agentcontainers/execer.go Outdated
// Preserve the prior quoting behavior on Windows. This path was
// already non-functional: the selected shell is usually PowerShell
// (see usershell.Get), which rejects the hardcoded cmd.exe "/c"
// switch. A correct Windows implementation is deferred.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

General note on comment hygiene: If this is deferred, it needs to be a ticket rather than a code comment. Future plans may be expressed in comments if they are helpful in the future design/implementation, but we shouldn't be using them as a poor substitute for an issue tracker.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack, this is all removed now.

Comment thread agent/agentcontainers/execer.go Outdated
args = append(args, fmt.Sprintf("%q", arg))
cmdArgs := append([]string{inName}, inArgs...)
if runtime.GOOS == "windows" {
// Preserve the prior quoting behavior on Windows. This path was

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My best guess is this was about leaving the door open to expand support to using Dev Containers on not just a Linux-based host workspace, but also Windows workspaces given the presence of Docker.

But unless there's motivation to actually do that it's just dead code and we should remove it and disable the tests for Windows.

Comment thread agent/agentcontainers/execer.go Outdated
// prevents arguments containing shell metacharacters such as $, `, and
// quotes from being interpreted (e.g. command substitution). The first
// argument after the script ($0) is a placeholder for the shell name.
args = append([]string{"-c", `"$@"`, shell}, cmdArgs...)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
args = append([]string{"-c", `"$@"`, shell}, cmdArgs...)
args = append([]string{"-c", `"$@"`, ""}, cmdArgs...)

Let's not make it seem like argv0 plays a role here as it is discarded anyway.

This solution is somewhat clever, but it does e.g. break shell script usage which could originate from our own implementation. I'm not sure if there is any such usage right now, but the result would be surprising (compared to other execer implementations).

I'm wondering if this is the correct level to apply this fix, or if we should handle it at the call site containing user inputs. Granted this fixes the class of bug even if future call-sites are introduced so I'm not going to block on this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point, I adopted your suggestion for argv0.

I thought about doing shell escaping at a higher level, but this seemed like the right place to me from a security perspective. I figured if we need something to handle non-user input that contains variables/command substitution etc we could create a separate function for that.

RE: internal shell script usage, I audited callers and they all appear to pass literal args. The only one that stood out was an inline substitution, but it targets an in-container shell

if _, err := api.ccli.ExecAs(ctx, container.ID, "root", "/bin/sh", "-c", fmt.Sprintf("chown $(id -u):$(id -g) %s", coderPathInsideContainer)); err != nil {

Comment thread agent/agentcontainers/execer.go Outdated
// switch. A correct Windows implementation is deferred.
var quoted []string
for _, arg := range cmdArgs {
quoted = append(quoted, fmt.Sprintf("%q", arg))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we keep the "$@" ... implementation, then I don't think this quoting is necessary. In fact, it probably breaks the implementation.

/bin/sh -c '"$@"' "" '"echo"' foo bar
: 1: "echo": not found

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is deleted now since I decided to remove the Windows handling 😄

@zedkipp zedkipp requested a review from mafredri June 11, 2026 15:26
// prevents arguments containing shell metacharacters such as $, `, and
// quotes from being interpreted (e.g. command substitution). The token
// before them fills $0, which "$@" never references, so it is discarded.
// This assumes a POSIX shell; Windows is not supported here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't seem like we have anything preventing this from executing on Windows upstream of this call, so I was concerned that a similar vulnerability might existing for command parsing on powershell.exe or cmd.exe using this new approach, but it appears that the $@ will cause a parse error on powershell. This is tested via a custom harness built with Claude that I can't seem to attach here - I'll DM you. I haven't checked cmd.exe, nor am I sure if something structural doesn't prevent this running on windows in the first place.

Ideally we would harden the POSIX requirement, but that's tricky.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know the exact failure mode if someone tried Windows for this, but I do see we assume linux for devcontainers when creating the sub-agent (proc.agent.OperatingSystem is only assigned a value on this line)

I'm not planning to dig any deeper on Windows here because it's not supported.

@f0ssel f0ssel requested a review from sreya June 11, 2026 18:27
@f0ssel f0ssel merged commit 112c921 into main Jun 11, 2026
32 checks passed
@f0ssel f0ssel deleted the zedkipp/plat-261-docker-interoplation branch June 11, 2026 18:56
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 11, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants