From 297a693ccc6485a92c6059b9f869969c451c5de5 Mon Sep 17 00:00:00 2001 From: seagochen Date: Thu, 26 Feb 2026 17:24:12 +0900 Subject: [PATCH] fix: prevent cd traversal (../../..) from escaping workspace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After filepath.Clean resolves ../ sequences, verify the resulting path is still within the workspace directory. If not, silently redirect to workspace root — same behaviour as cd / and cd ~. Co-Authored-By: Claude Opus 4.6 --- pkg/agent/loop.go | 5 +++++ pkg/agent/loop_test.go | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 497c12285..d525dfebc 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -1426,6 +1426,11 @@ func (al *AgentLoop) handleCdCommand(content, sessionKey string, agent *AgentIns target = filepath.Clean(target) + // Prevent traversal outside workspace via ../ + if !strings.HasPrefix(target, workspace) { + target = workspace + } + info, err := os.Stat(target) if err != nil { return fmt.Sprintf("cd: %s: No such file or directory", target) diff --git a/pkg/agent/loop_test.go b/pkg/agent/loop_test.go index 63c0754f2..6dc1ccc7f 100644 --- a/pkg/agent/loop_test.go +++ b/pkg/agent/loop_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "testing" "time" @@ -632,6 +633,42 @@ func TestAgentLoop_ContextExhaustionRetry(t *testing.T) { } } +// TestHandleCdCommand_TraversalBlocked verifies that cd ../../.. cannot escape workspace. +func TestHandleCdCommand_TraversalBlocked(t *testing.T) { + workspace := t.TempDir() + cfg := &config.Config{ + Agents: config.AgentsConfig{ + Defaults: config.AgentDefaults{ + Workspace: workspace, + Model: "test-model", + MaxTokens: 4096, + }, + }, + } + msgBus := bus.NewMessageBus() + provider := &mockProvider{} + al := NewAgentLoop(cfg, msgBus, provider) + agent := al.registry.GetDefaultAgent() + + // Set working dir to a subdir inside workspace + subdir := filepath.Join(workspace, "a", "b") + os.MkdirAll(subdir, 0o755) + al.setSessionWorkDir("test", subdir) + + // Try to escape via ../../../.. + result := al.handleCdCommand("cd ../../../..", "test", agent) + workDir := al.getSessionWorkDir("test") + + if !strings.HasPrefix(workDir, workspace) { + t.Errorf("cd traversal escaped workspace: workDir=%s, workspace=%s", workDir, workspace) + } + // Should land in workspace, not outside + if workDir != workspace { + t.Errorf("Expected workDir=%s, got %s", workspace, workDir) + } + _ = result +} + // TestHandleExtensionCommand_EmojiPassthrough verifies that emoji-like // messages starting with : are not intercepted as commands. func TestHandleExtensionCommand_EmojiPassthrough(t *testing.T) {