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) {