From 69615f31469a88100ce7652eefb3068c42779831 Mon Sep 17 00:00:00 2001 From: Stephane Bouchet Date: Thu, 5 Nov 2026 15:03:56 +0100 Subject: [PATCH] Re-validate cwd after file:// decode to prevent command injection The cwd command-injection fix (a78541c) validated the raw cwd value at the API boundary, but ResolveCmd percent-decodes file:// paths via url.Parse afterwards. Since '%' is not in the metacharacter blocklist, an encoded payload such as "file:///dev/null%3Bid" passed validation, decoded to "/dev/null;id", and was interpolated unquoted into the "sh -c" command (TOCTOU). Re-validate the decoded cwd in ResolveCmd, the chokepoint all exec entry points funnel through, so no shell metacharacter can reach the command regardless of encoding. Add regression tests for the known percent-encoded bypass payloads. Co-Authored-By: Claude Opus 4.8 Signed-off-by: Stephane Bouchet --- exec/command_resolver.go | 4 ++++ exec/command_resolver_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/exec/command_resolver.go b/exec/command_resolver.go index 201aac89b..09334e80e 100644 --- a/exec/command_resolver.go +++ b/exec/command_resolver.go @@ -20,6 +20,7 @@ import ( "github.com/sirupsen/logrus" "github.com/eclipse-che/che-machine-exec/api/model" + "github.com/eclipse-che/che-machine-exec/api/validation" "github.com/eclipse-che/che-machine-exec/client" exec_info "github.com/eclipse-che/che-machine-exec/exec-info" "github.com/eclipse-che/che-machine-exec/shell" @@ -79,6 +80,9 @@ func (cmdRslv *CmdResolver) ResolveCmd(exec model.MachineExec, containerInfo *mo exec.Cwd = res.Path } } + if err := validation.ValidateNoShellMetacharacters(exec.Cwd, "cwd"); err != nil { + return nil, err + } cdCommand = fmt.Sprintf("cd %s; ", exec.Cwd) } diff --git a/exec/command_resolver_test.go b/exec/command_resolver_test.go index 6879313db..dbf127306 100644 --- a/exec/command_resolver_test.go +++ b/exec/command_resolver_test.go @@ -226,6 +226,35 @@ func TestShouldUseDefaultShellToLaunchCommandWithCwdWhenShellIsNotDefined(t *tes assert.Equal(t, []string{"sh", "-c", "cd /projects/testprj; yarn run build"}, resolvedCmd) } +func TestShouldRejectCwdWithPercentEncodedShellMetacharacters(t *testing.T) { + // Regression test for the incomplete fix of CVE-2026-114278 / GHSA-rq96-f9wc-rppm. + // url.Parse percent-decodes the file:// path, so encoded shell metacharacters + // must not survive into the resolved "sh -c" command. + maliciousCwds := []string{ + "file:///dev/null%3Bid", // %3B -> ; + "file:///tmp%7Cid", // %7C -> | + "file:///tmp/%60id%60", // %60 -> ` + "file:///tmp/%24%28id%29", // %24%28..%29 -> $(..) + "file:///tmp%0Aid", // %0A -> newline + } + + for _, cwd := range maliciousCwds { + t.Run(cwd, func(t *testing.T) { + exec := model.MachineExec{ + Type: "shell", + Cmd: []string{"sh", "-c", "echo terminal_ready"}, + Cwd: cwd, + } + + cmdResolver := &CmdResolver{&mocks.ContainerShellDetector{}, &mocks.InfoExecCreator{}} + resolvedCmd, err := cmdResolver.ResolveCmd(exec, containerInfo) + + assert.Error(t, err, "expected ResolveCmd to reject percent-encoded injection in cwd") + assert.Nil(t, resolvedCmd) + }) + } +} + func TestThrowAnErrorWhenEvenDefaultShellDoesNotWork(t *testing.T) { shellDetectorMock := &mocks.ContainerShellDetector{} shellDetectorMock.On("DetectShell", containerInfo).Return("/sbin/nologin", nil)