fix(security): block critical symlink workspace escape (#188)
This commit is contained in:
@@ -29,13 +29,54 @@ func validatePath(path, workspace string, restrict bool) (string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if restrict && !strings.HasPrefix(absPath, absWorkspace) {
|
if restrict {
|
||||||
|
if !isWithinWorkspace(absPath, absWorkspace) {
|
||||||
return "", fmt.Errorf("access denied: path is outside the workspace")
|
return "", fmt.Errorf("access denied: path is outside the workspace")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
workspaceReal := absWorkspace
|
||||||
|
if resolved, err := filepath.EvalSymlinks(absWorkspace); err == nil {
|
||||||
|
workspaceReal = resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
if resolved, err := filepath.EvalSymlinks(absPath); err == nil {
|
||||||
|
if !isWithinWorkspace(resolved, workspaceReal) {
|
||||||
|
return "", fmt.Errorf("access denied: symlink resolves outside workspace")
|
||||||
|
}
|
||||||
|
} else if os.IsNotExist(err) {
|
||||||
|
if parentResolved, err := resolveExistingAncestor(filepath.Dir(absPath)); err == nil {
|
||||||
|
if !isWithinWorkspace(parentResolved, workspaceReal) {
|
||||||
|
return "", fmt.Errorf("access denied: symlink resolves outside workspace")
|
||||||
|
}
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return "", fmt.Errorf("failed to resolve path: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("failed to resolve path: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return absPath, nil
|
return absPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveExistingAncestor(path string) (string, error) {
|
||||||
|
for current := filepath.Clean(path); ; current = filepath.Dir(current) {
|
||||||
|
if resolved, err := filepath.EvalSymlinks(current); err == nil {
|
||||||
|
return resolved, nil
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if filepath.Dir(current) == current {
|
||||||
|
return "", os.ErrNotExist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isWithinWorkspace(candidate, workspace string) bool {
|
||||||
|
rel, err := filepath.Rel(filepath.Clean(workspace), filepath.Clean(candidate))
|
||||||
|
return err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator))
|
||||||
|
}
|
||||||
|
|
||||||
type ReadFileTool struct {
|
type ReadFileTool struct {
|
||||||
workspace string
|
workspace string
|
||||||
restrict bool
|
restrict bool
|
||||||
|
|||||||
@@ -247,3 +247,35 @@ func TestFilesystemTool_ListDir_DefaultPath(t *testing.T) {
|
|||||||
t.Errorf("Expected success with default path '.', got IsError=true: %s", result.ForLLM)
|
t.Errorf("Expected success with default path '.', got IsError=true: %s", result.ForLLM)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Block paths that look inside workspace but point outside via symlink.
|
||||||
|
func TestFilesystemTool_ReadFile_RejectsSymlinkEscape(t *testing.T) {
|
||||||
|
|
||||||
|
root := t.TempDir()
|
||||||
|
workspace := filepath.Join(root, "workspace")
|
||||||
|
if err := os.MkdirAll(workspace, 0755); err != nil {
|
||||||
|
t.Fatalf("failed to create workspace: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secret := filepath.Join(root, "secret.txt")
|
||||||
|
if err := os.WriteFile(secret, []byte("top secret"), 0644); err != nil {
|
||||||
|
t.Fatalf("failed to write secret file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
link := filepath.Join(workspace, "leak.txt")
|
||||||
|
if err := os.Symlink(secret, link); err != nil {
|
||||||
|
t.Skipf("symlink not supported in this environment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tool := NewReadFileTool(workspace, true)
|
||||||
|
result := tool.Execute(context.Background(), map[string]interface{}{
|
||||||
|
"path": link,
|
||||||
|
})
|
||||||
|
|
||||||
|
if !result.IsError {
|
||||||
|
t.Fatalf("expected symlink escape to be blocked")
|
||||||
|
}
|
||||||
|
if !strings.Contains(result.ForLLM, "symlink resolves outside workspace") {
|
||||||
|
t.Fatalf("expected symlink escape error, got: %s", result.ForLLM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user