fix(exec): ignore URL segments in workspace guard
This commit is contained in:
parent
4a8a2e9c23
commit
89bfcd54c8
2 changed files with 80 additions and 2 deletions
|
|
@ -12,6 +12,8 @@ import (
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/pkg/config"
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"github.com/sipeed/picoclaw/pkg/constants"
|
"github.com/sipeed/picoclaw/pkg/constants"
|
||||||
|
|
@ -78,7 +80,8 @@ var (
|
||||||
regexp.MustCompile(`\bsource\s+.*\.sh\b`),
|
regexp.MustCompile(`\bsource\s+.*\.sh\b`),
|
||||||
}
|
}
|
||||||
|
|
||||||
// absolutePathPattern matches absolute file paths in commands (Unix and Windows).
|
// absolutePathPattern matches path-like substrings in commands (Unix and Windows).
|
||||||
|
// A separate boundary check is applied before treating a match as a filesystem path.
|
||||||
absolutePathPattern = regexp.MustCompile(`[A-Za-z]:\\[^\\\"']+|/[^\s\"']+`)
|
absolutePathPattern = regexp.MustCompile(`[A-Za-z]:\\[^\\\"']+|/[^\s\"']+`)
|
||||||
|
|
||||||
// safePaths are kernel pseudo-devices that are always safe to reference in
|
// safePaths are kernel pseudo-devices that are always safe to reference in
|
||||||
|
|
@ -373,7 +376,7 @@ func (t *ExecTool) guardCommand(command, cwd string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
matches := absolutePathPattern.FindAllString(cmd, -1)
|
matches := findAbsolutePathMatches(cmd)
|
||||||
|
|
||||||
for _, raw := range matches {
|
for _, raw := range matches {
|
||||||
p, err := filepath.Abs(raw)
|
p, err := filepath.Abs(raw)
|
||||||
|
|
@ -399,6 +402,37 @@ func (t *ExecTool) guardCommand(command, cwd string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func findAbsolutePathMatches(command string) []string {
|
||||||
|
indexes := absolutePathPattern.FindAllStringIndex(command, -1)
|
||||||
|
if len(indexes) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := make([]string, 0, len(indexes))
|
||||||
|
for _, idx := range indexes {
|
||||||
|
start := idx[0]
|
||||||
|
if !isPathBoundary(command, start) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
matches = append(matches, command[start:idx[1]])
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches
|
||||||
|
}
|
||||||
|
|
||||||
|
func isPathBoundary(command string, start int) bool {
|
||||||
|
if start <= 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
r, _ := utf8.DecodeLastRuneInString(command[:start])
|
||||||
|
if unicode.IsSpace(r) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.ContainsRune(`"'=<>|&;()[]{},`, r)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *ExecTool) SetTimeout(timeout time.Duration) {
|
func (t *ExecTool) SetTimeout(timeout time.Duration) {
|
||||||
t.timeout = timeout
|
t.timeout = timeout
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -489,6 +489,50 @@ func TestShellTool_SafePathsInWorkspaceRestriction(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestShellTool_GuardCommand_IgnoresURLPathSegments(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
tool, err := NewExecTool(tmpDir, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to configure exec tool: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
commands := []string{
|
||||||
|
`curl -s "wttr.in/Beijing?T"`,
|
||||||
|
`curl -s https://example.com/api/v1/weather`,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cmd := range commands {
|
||||||
|
if got := tool.guardCommand(cmd, tmpDir); got != "" {
|
||||||
|
t.Fatalf("guardCommand(%q) = %q, want empty", cmd, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShellTool_GuardCommand_BlocksAbsolutePathOutsideWorkspace(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
workspace := filepath.Join(root, "workspace")
|
||||||
|
outsideDir := filepath.Join(root, "outside")
|
||||||
|
outsideFile := filepath.Join(outsideDir, "secret.txt")
|
||||||
|
if err := os.MkdirAll(workspace, 0o755); err != nil {
|
||||||
|
t.Fatalf("failed to create workspace: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(outsideDir, 0o755); err != nil {
|
||||||
|
t.Fatalf("failed to create outside dir: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(outsideFile, []byte("secret"), 0o644); err != nil {
|
||||||
|
t.Fatalf("failed to create outside file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tool, err := NewExecTool(workspace, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to configure exec tool: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := tool.guardCommand(`cat "`+outsideFile+`"`, workspace); !strings.Contains(got, "path outside working dir") {
|
||||||
|
t.Fatalf("guardCommand should block outside path, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestShellTool_CustomAllowPatterns verifies that custom allow patterns exempt
|
// TestShellTool_CustomAllowPatterns verifies that custom allow patterns exempt
|
||||||
// commands from deny pattern checks.
|
// commands from deny pattern checks.
|
||||||
func TestShellTool_CustomAllowPatterns(t *testing.T) {
|
func TestShellTool_CustomAllowPatterns(t *testing.T) {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue