fix: prevent cd traversal (../../..) from escaping workspace

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 <noreply@anthropic.com>
This commit is contained in:
seagochen 2026-02-26 17:24:12 +09:00
parent cc3b58d9da
commit 297a693ccc
2 changed files with 42 additions and 0 deletions

View file

@ -1426,6 +1426,11 @@ func (al *AgentLoop) handleCdCommand(content, sessionKey string, agent *AgentIns
target = filepath.Clean(target) target = filepath.Clean(target)
// Prevent traversal outside workspace via ../
if !strings.HasPrefix(target, workspace) {
target = workspace
}
info, err := os.Stat(target) info, err := os.Stat(target)
if err != nil { if err != nil {
return fmt.Sprintf("cd: %s: No such file or directory", target) return fmt.Sprintf("cd: %s: No such file or directory", target)

View file

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"time" "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 // TestHandleExtensionCommand_EmojiPassthrough verifies that emoji-like
// messages starting with : are not intercepted as commands. // messages starting with : are not intercepted as commands.
func TestHandleExtensionCommand_EmojiPassthrough(t *testing.T) { func TestHandleExtensionCommand_EmojiPassthrough(t *testing.T) {