Merge pull request #2 from YspCoder/codex/conduct-risk-and-bug-assessment-4fecjc

Harden shell command safety guards and add tests for root-path rm variants
This commit is contained in:
野生派Coder~
2026-02-14 13:58:38 +08:00
committed by GitHub
2 changed files with 68 additions and 6 deletions

View File

@@ -15,7 +15,7 @@ import (
"clawgo/pkg/logger"
)
var blockedRootWipePattern = regexp.MustCompile(`(?i)(^|[;&|\n])\s*rm\s+-rf\s+/\s*($|[;&|\n])`)
var blockedRootWipePattern = regexp.MustCompile(`(?i)(^|[;&|\n])\s*rm\b[^\n;&|]*\s(?:'/'|"/"|/)(?:\s|$)`)
type ExecTool struct {
workingDir string
@@ -31,7 +31,12 @@ type ExecTool struct {
func NewExecTool(cfg config.ShellConfig, workspace string) *ExecTool {
denyPatterns := make([]*regexp.Regexp, 0, len(cfg.DeniedCmds))
for _, p := range cfg.DeniedCmds {
denyPatterns = append(denyPatterns, regexp.MustCompile(`\b`+regexp.QuoteMeta(p)+`\b`))
denyPatterns = append(denyPatterns, regexp.MustCompile(`(?i)\b`+regexp.QuoteMeta(p)+`\b`))
}
allowPatterns := make([]*regexp.Regexp, 0, len(cfg.AllowedCmds))
for _, p := range cfg.AllowedCmds {
allowPatterns = append(allowPatterns, regexp.MustCompile(`(?i)\b`+regexp.QuoteMeta(p)+`\b`))
}
allowPatterns := make([]*regexp.Regexp, 0, len(cfg.AllowedCmds))
@@ -155,11 +160,11 @@ func (t *ExecTool) guardCommand(command, cwd string) string {
lower := strings.ToLower(cmd)
if blockedRootWipePattern.MatchString(lower) {
return "Command blocked by safety guard (rm -rf / is forbidden)"
return "Command blocked by safety guard (removing root path / is forbidden)"
}
for _, pattern := range t.denyPatterns {
if pattern.MatchString(lower) {
if pattern.MatchString(cmd) {
return "Command blocked by safety guard (dangerous pattern detected)"
}
}
@@ -167,7 +172,7 @@ func (t *ExecTool) guardCommand(command, cwd string) string {
if len(t.allowPatterns) > 0 {
allowed := false
for _, pattern := range t.allowPatterns {
if pattern.MatchString(lower) {
if pattern.MatchString(cmd) {
allowed = true
break
}
@@ -221,7 +226,7 @@ func (t *ExecTool) SetRestrictToWorkspace(restrict bool) {
func (t *ExecTool) SetAllowPatterns(patterns []string) error {
t.allowPatterns = make([]*regexp.Regexp, 0, len(patterns))
for _, p := range patterns {
re, err := regexp.Compile(p)
re, err := regexp.Compile("(?i)" + p)
if err != nil {
return fmt.Errorf("invalid allow pattern %q: %w", p, err)
}
@@ -264,6 +269,7 @@ func (t *ExecTool) applyRiskGate(command string, force bool) (string, string) {
if dryRunCmd, ok := buildDryRunCommand(command); ok {
return "Risk gate: dry-run required first. Review output, then execute intentionally with force=true.", dryRunCmd
}
return "Error: destructive command requires explicit force=true because no dry-run strategy is available.", ""
}
return "", ""
}

View File

@@ -61,3 +61,59 @@ func TestGuardCommand_BlocksCommandNotInAllowlist(t *testing.T) {
t.Fatalf("expected allowed command to pass guard, got %q", msg)
}
}
func TestGuardCommand_AllowlistIsCaseInsensitive(t *testing.T) {
tool := NewExecTool(config.ShellConfig{AllowedCmds: []string{"ECHO"}}, ".")
if msg := tool.guardCommand("echo hi", "."); msg != "" {
t.Fatalf("expected case-insensitive allowlist match, got %q", msg)
}
}
func TestGuardCommand_DenylistIsCaseInsensitive(t *testing.T) {
tool := NewExecTool(config.ShellConfig{DeniedCmds: []string{"RM"}}, ".")
if msg := tool.guardCommand("rm -f tmp.txt", "."); msg == "" {
t.Fatal("expected case-insensitive denylist match to block command")
}
}
func TestApplyRiskGate_RequireDryRunWithoutStrategyStillBlocks(t *testing.T) {
tool := &ExecTool{riskCfg: config.RiskConfig{
Enabled: true,
AllowDestructive: true,
RequireDryRun: true,
RequireForceFlag: false,
}}
msg, dryRun := tool.applyRiskGate("rm -rf tmp", false)
if msg == "" {
t.Fatal("expected destructive command without dry-run strategy to be blocked")
}
if dryRun != "" {
t.Fatalf("expected no dry-run command for rm -rf, got %q", dryRun)
}
}
func TestSetAllowPatterns_IsCaseInsensitive(t *testing.T) {
tool := &ExecTool{}
if err := tool.SetAllowPatterns([]string{`^ECHO\b`}); err != nil {
t.Fatalf("SetAllowPatterns returned error: %v", err)
}
if msg := tool.guardCommand("echo hi", "."); msg != "" {
t.Fatalf("expected case-insensitive allow pattern to match, got %q", msg)
}
}
func TestGuardCommand_BlocksRootWipeVariants(t *testing.T) {
tool := &ExecTool{}
cases := []string{
"rm -rf /",
"rm -fr /",
"rm --no-preserve-root -rf /",
}
for _, c := range cases {
if msg := tool.guardCommand(c, "."); msg == "" {
t.Fatalf("expected root wipe variant to be blocked: %s", c)
}
}
}