fix(tools): address copilot review feedback
- Fix absolutePathPattern to support single quotes and short flags - Add comment about file:// and CustomAllowPatterns interaction - Add prefix constraint to /proc patterns to avoid false positives from URLs - Fix path traversal check to strip URLs first - Replace real network commands with echo in tests - Add TestShellTool_QuotedAndFlagPathsBlocked for new coverage - Remove TestShellTool_RemotePathInSSHAllowed (no assertions)
This commit is contained in:
parent
e0210e48c8
commit
13ac4a6851
2 changed files with 49 additions and 39 deletions
|
|
@ -74,21 +74,25 @@ var (
|
||||||
regexp.MustCompile(`\bssh\b.*@`),
|
regexp.MustCompile(`\bssh\b.*@`),
|
||||||
regexp.MustCompile(`\beval\b`),
|
regexp.MustCompile(`\beval\b`),
|
||||||
regexp.MustCompile(`\bsource\s+.*\.sh\b`),
|
regexp.MustCompile(`\bsource\s+.*\.sh\b`),
|
||||||
// file:// protocol allows reading local files outside workspace
|
// file:// protocol allows reading local files outside workspace.
|
||||||
|
// Note: deny patterns are skipped when CustomAllowPatterns match,
|
||||||
|
// so configuring custom allow patterns may disable this protection.
|
||||||
regexp.MustCompile(`\bfile://`),
|
regexp.MustCompile(`\bfile://`),
|
||||||
// Sensitive /proc paths that may expose credentials or process info
|
// Sensitive /proc paths that may expose credentials or process info.
|
||||||
regexp.MustCompile(`/proc/self/environ\b`),
|
// Prefix constraint prevents false positives from URLs containing /proc.
|
||||||
regexp.MustCompile(`/proc/self/cmdline\b`),
|
regexp.MustCompile(`(?:^|[\s=\"])/proc/self/environ\b`),
|
||||||
regexp.MustCompile(`/proc/self/fd/\d+`),
|
regexp.MustCompile(`(?:^|[\s=\"])/proc/self/cmdline\b`),
|
||||||
regexp.MustCompile(`/proc/self/mem\b`),
|
regexp.MustCompile(`(?:^|[\s=\"])/proc/self/fd/\d+`),
|
||||||
regexp.MustCompile(`/proc/self/maps\b`),
|
regexp.MustCompile(`(?:^|[\s=\"])/proc/self/mem\b`),
|
||||||
regexp.MustCompile(`/proc/\d+/environ\b`),
|
regexp.MustCompile(`(?:^|[\s=\"])/proc/self/maps\b`),
|
||||||
|
regexp.MustCompile(`(?:^|[\s=\"])/proc/\d+/environ\b`),
|
||||||
}
|
}
|
||||||
|
|
||||||
// absolutePathPattern matches absolute file paths (Unix and Windows).
|
// absolutePathPattern matches absolute file paths (Unix and Windows).
|
||||||
// Unix paths must be preceded by whitespace, '=', '"' or start of string
|
// Unix paths must be preceded by whitespace, '=', '"', "'", a short flag
|
||||||
// to avoid matching paths inside relative paths like "./download/file".
|
// (e.g., "-o"), or start of string to avoid matching paths inside URLs
|
||||||
absolutePathPattern = regexp.MustCompile(`(?:^|[\s="])(/[^\s\"']+)|([A-Za-z]:\\[^\\\"']+)`)
|
// or relative paths like "./download/file".
|
||||||
|
absolutePathPattern = regexp.MustCompile(`(?:^|[\s=\"']|-[A-Za-z]*)(/[^\s\"']+)|([A-Za-z]:\\[^\\\"']+)`)
|
||||||
|
|
||||||
// urlPattern matches URLs (http://, https://, ftp://, sftp://, git+https://, etc.)
|
// urlPattern matches URLs (http://, https://, ftp://, sftp://, git+https://, etc.)
|
||||||
urlPattern = regexp.MustCompile(`(?i)[a-z][a-z0-9+.-]*://[^\s\"']+`)
|
urlPattern = regexp.MustCompile(`(?i)[a-z][a-z0-9+.-]*://[^\s\"']+`)
|
||||||
|
|
@ -346,7 +350,10 @@ func (t *ExecTool) guardCommand(command, cwd string) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
if t.restrictToWorkspace {
|
if t.restrictToWorkspace {
|
||||||
if strings.Contains(cmd, "..\\") || strings.Contains(cmd, "../") {
|
// Remove URLs before checking path traversal to avoid false positives
|
||||||
|
// from URLs like https://example.com/a/../b
|
||||||
|
cmdWithoutURLs := urlPattern.ReplaceAllString(cmd, "")
|
||||||
|
if strings.Contains(cmdWithoutURLs, "..\\") || strings.Contains(cmdWithoutURLs, "../") {
|
||||||
return "Command blocked by safety guard (path traversal detected)"
|
return "Command blocked by safety guard (path traversal detected)"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -355,9 +362,6 @@ func (t *ExecTool) guardCommand(command, cwd string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove URLs before matching paths to avoid treating https://github.com as a file path
|
|
||||||
cmdWithoutURLs := urlPattern.ReplaceAllString(cmd, "")
|
|
||||||
|
|
||||||
submatches := absolutePathPattern.FindAllStringSubmatch(cmdWithoutURLs, -1)
|
submatches := absolutePathPattern.FindAllStringSubmatch(cmdWithoutURLs, -1)
|
||||||
for _, match := range submatches {
|
for _, match := range submatches {
|
||||||
raw := match[1] // Unix path
|
raw := match[1] // Unix path
|
||||||
|
|
|
||||||
|
|
@ -453,48 +453,54 @@ func TestShellTool_RemoteURLsAllowed(t *testing.T) {
|
||||||
t.Fatalf("unable to configure exec tool: %s", err)
|
t.Fatalf("unable to configure exec tool: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Commands with remote URLs should not be blocked by path check.
|
// Use harmless echo commands that still contain the same URL patterns,
|
||||||
|
// so we exercise the guard logic without performing network operations.
|
||||||
allowed := []string{
|
allowed := []string{
|
||||||
"curl https://github.com/user/repo",
|
"echo https://github.com/user/repo",
|
||||||
"wget http://example.com/file.tar.gz",
|
"echo http://example.com/file.tar.gz",
|
||||||
"git clone https://github.com/user/repo.git",
|
"echo https://github.com/user/repo.git",
|
||||||
"agent-browser open https://github.com",
|
"echo https://github.com",
|
||||||
"pip install git+https://github.com/user/pkg",
|
"echo git+https://github.com/user/pkg",
|
||||||
"curl ftp://ftp.example.com/file",
|
"echo ftp://ftp.example.com/file",
|
||||||
"curl sftp://server.com/path/file",
|
"echo sftp://server.com/path/file",
|
||||||
"curl HTTPS://github.com/user/repo", // case insensitive
|
"echo HTTPS://github.com/user/repo", // case insensitive
|
||||||
"curl HTTP://example.com/file", // case insensitive
|
"echo HTTP://example.com/file", // case insensitive
|
||||||
|
// URL with path traversal should not be blocked (it's in the URL)
|
||||||
|
"echo https://example.com/a/../b",
|
||||||
|
"echo https://example.com/proc/self/environ",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cmd := range allowed {
|
for _, cmd := range allowed {
|
||||||
result := tool.Execute(context.Background(), map[string]any{"command": cmd})
|
result := tool.Execute(context.Background(), map[string]any{"command": cmd})
|
||||||
if result.IsError && strings.Contains(result.ForLLM, "path outside working dir") {
|
// URL commands should not be blocked by the workspace/path guard.
|
||||||
t.Errorf("URL command should not be blocked by path check: %s\n error: %s", cmd, result.ForLLM)
|
if result.IsError {
|
||||||
|
t.Errorf("URL command should not fail or be blocked: %s\n error: %s", cmd, result.ForLLM)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestShellTool_RemotePathInSSHAllowed verifies that remote paths in SSH/SCP commands
|
// TestShellTool_QuotedAndFlagPathsBlocked verifies that paths with quotes
|
||||||
// are not blocked (they refer to remote systems, not local files).
|
// or after short flags are properly detected and blocked.
|
||||||
func TestShellTool_RemotePathInSSHAllowed(t *testing.T) {
|
func TestShellTool_QuotedAndFlagPathsBlocked(t *testing.T) {
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
tool, err := NewExecTool(tmpDir, true)
|
tool, err := NewExecTool(tmpDir, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to configure exec tool: %s", err)
|
t.Fatalf("unable to configure exec tool: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSH/SCP with remote paths - the /path/file is on the remote host.
|
// Paths with single quotes or after short flags should be blocked.
|
||||||
// Note: "ssh user@host" is blocked by deny pattern, so we test the path logic
|
blocked := []string{
|
||||||
// only by checking that /path/file in scp/rsync syntax is not blocked.
|
"cat '/etc/passwd'",
|
||||||
allowed := []string{
|
"cat \"/etc/passwd\"",
|
||||||
"rsync /local/file ./dest", // local absolute path in workspace check
|
"curl -o/tmp/file https://example.com",
|
||||||
"rsync ./src/ /tmp/", // /tmp/ should be blocked
|
"tar -C/etc -xf archive.tar",
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, cmd := range allowed {
|
for _, cmd := range blocked {
|
||||||
result := tool.Execute(context.Background(), map[string]any{"command": cmd})
|
result := tool.Execute(context.Background(), map[string]any{"command": cmd})
|
||||||
// We just verify no crash and proper handling
|
if !result.IsError {
|
||||||
_ = result
|
t.Errorf("command with path outside workspace should be blocked: %s", cmd)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue