address @sky5454 request to compile regex patterns in a txt file

This commit is contained in:
Goksu Ceylan 2026-03-06 09:36:16 -05:00
parent 29be5477a6
commit 3045cad1b2
3 changed files with 122 additions and 54 deletions

View file

@ -0,0 +1,47 @@
# Dangerous commands
\brm\s+-[rf]{1,2}\b
\bdel\s+/[fq]\b
\brmdir\s+/s\b
\b(format|mkfs|diskpart)\b\s
\bdd\s+if=
>\s*/dev/sd[a-z]\b
\b(shutdown|reboot|poweroff)\b
:\(\)\s*\{.*\};\s*:
\$\([^)]+\)
\$\{[^}]+\}
`[^`]+`
\|\s*sh\b
\|\s*bash\b
\|\s*/\S*(bash|sh|zsh|ksh|fish|csh|tcsh)\b
;\s*rm\s+-[rf]
&&\s*rm\s+-[rf]
\|\|\s*rm\s+-[rf]
>\s*/dev/null\s*>&?\s*\d?
<<\s*EOF
<<<
\$\(\s*cat\s+
\$\(\s*curl\s+
\$\(\s*wget\s+
\$\(\s*which\s+
\bsudo\b
\bsu\b.*-c\b
\bchmod\s+[0-7]{3,4}\b
\bchown\b
\bpkill\b
\bkillall\b
\bkill\s+-[9]\b
\bcurl\b.*\|\s*(sh|bash)
\bwget\b.*\|\s*(sh|bash)
\bnpm\s+install\s+-g\b
\bpip\s+install\s+--user\b
\bapt\s+(install|remove|purge)\b
\byum\s+(install|remove)\b
\bdnf\s+(install|remove)\b
\bdocker\s+run\b
\bdocker\s+exec\b
\bgit\s+push\b
\bgit\s+force\b
\bssh\b.*@
\beval\b
\bsource\s+\S+
(?:^|&&|\|\||;)\s*\.\s+\S

View file

@ -3,6 +3,7 @@ package tools
import ( import (
"bytes" "bytes"
"context" "context"
_ "embed"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@ -24,53 +25,42 @@ type ExecTool struct {
restrictToWorkspace bool restrictToWorkspace bool
} }
var defaultDenyPatterns = []*regexp.Regexp{ //go:embed default_deny_patterns.txt
regexp.MustCompile(`\brm\s+-[rf]{1,2}\b`), var defaultDenyPatternsText string
regexp.MustCompile(`\bdel\s+/[fq]\b`),
regexp.MustCompile(`\brmdir\s+/s\b`), var defaultDenyPatterns = mustCompileRegexPatterns(parsePatternLines(defaultDenyPatternsText))
regexp.MustCompile(`\b(format|mkfs|diskpart)\b\s`), // Match disk wiping commands (must be followed by space/args)
regexp.MustCompile(`\bdd\s+if=`), func parsePatternLines(text string) []string {
regexp.MustCompile(`>\s*/dev/sd[a-z]\b`), // Block writes to disk devices (but allow /dev/null) lines := strings.Split(text, "\n")
regexp.MustCompile(`\b(shutdown|reboot|poweroff)\b`), patterns := make([]string, 0, len(lines))
regexp.MustCompile(`:\(\)\s*\{.*\};\s*:`), for _, line := range lines {
regexp.MustCompile(`\$\([^)]+\)`), trimmed := strings.TrimSpace(line)
regexp.MustCompile(`\$\{[^}]+\}`), if trimmed == "" || strings.HasPrefix(trimmed, "#") {
regexp.MustCompile("`[^`]+`"), continue
regexp.MustCompile(`\|\s*sh\b`), }
regexp.MustCompile(`\|\s*bash\b`), patterns = append(patterns, trimmed)
regexp.MustCompile(`\|\s*/\S*(bash|sh|zsh|ksh|fish|csh|tcsh)\b`), // shell by full path: | /bin/bash, | /usr/bin/sh }
regexp.MustCompile(`;\s*rm\s+-[rf]`), return patterns
regexp.MustCompile(`&&\s*rm\s+-[rf]`), }
regexp.MustCompile(`\|\|\s*rm\s+-[rf]`),
regexp.MustCompile(`>\s*/dev/null\s*>&?\s*\d?`), func compileRegexPatterns(patterns []string) ([]*regexp.Regexp, error) {
regexp.MustCompile(`<<\s*EOF`), compiled := make([]*regexp.Regexp, 0, len(patterns))
regexp.MustCompile(`<<<`), // here-string: bash <<< "rm -rf /" for _, p := range patterns {
regexp.MustCompile(`\$\(\s*cat\s+`), re, err := regexp.Compile(p)
regexp.MustCompile(`\$\(\s*curl\s+`), if err != nil {
regexp.MustCompile(`\$\(\s*wget\s+`), return nil, fmt.Errorf("invalid pattern %q: %w", p, err)
regexp.MustCompile(`\$\(\s*which\s+`), }
regexp.MustCompile(`\bsudo\b`), compiled = append(compiled, re)
regexp.MustCompile(`\bsu\b.*-c\b`), // su -c / su root -c as sudo alternative }
regexp.MustCompile(`\bchmod\s+[0-7]{3,4}\b`), return compiled, nil
regexp.MustCompile(`\bchown\b`), }
regexp.MustCompile(`\bpkill\b`),
regexp.MustCompile(`\bkillall\b`), func mustCompileRegexPatterns(patterns []string) []*regexp.Regexp {
regexp.MustCompile(`\bkill\s+-[9]\b`), compiled, err := compileRegexPatterns(patterns)
regexp.MustCompile(`\bcurl\b.*\|\s*(sh|bash)`), if err != nil {
regexp.MustCompile(`\bwget\b.*\|\s*(sh|bash)`), panic("invalid default deny patterns: " + err.Error())
regexp.MustCompile(`\bnpm\s+install\s+-g\b`), }
regexp.MustCompile(`\bpip\s+install\s+--user\b`), return compiled
regexp.MustCompile(`\bapt\s+(install|remove|purge)\b`),
regexp.MustCompile(`\byum\s+(install|remove)\b`),
regexp.MustCompile(`\bdnf\s+(install|remove)\b`),
regexp.MustCompile(`\bdocker\s+run\b`),
regexp.MustCompile(`\bdocker\s+exec\b`),
regexp.MustCompile(`\bgit\s+push\b`),
regexp.MustCompile(`\bgit\s+force\b`),
regexp.MustCompile(`\bssh\b.*@`),
regexp.MustCompile(`\beval\b`),
regexp.MustCompile(`\bsource\s+\S+`), // was: \.sh\b — now catches any sourced file
regexp.MustCompile(`(?:^|&&|\|\||;)\s*\.\s+\S`), // dot-sourcing: . evil.sh / && . evil.sh
} }
func NewExecTool(workingDir string, restrict bool) *ExecTool { func NewExecTool(workingDir string, restrict bool) *ExecTool {
@ -324,13 +314,10 @@ func (t *ExecTool) SetRestrictToWorkspace(restrict bool) {
} }
func (t *ExecTool) SetAllowPatterns(patterns []string) error { func (t *ExecTool) SetAllowPatterns(patterns []string) error {
t.allowPatterns = make([]*regexp.Regexp, 0, len(patterns)) compiled, err := compileRegexPatterns(patterns)
for _, p := range patterns { if err != nil {
re, err := regexp.Compile(p) return fmt.Errorf("invalid allow pattern: %w", err)
if err != nil {
return fmt.Errorf("invalid allow pattern %q: %w", p, err)
}
t.allowPatterns = append(t.allowPatterns, re)
} }
t.allowPatterns = compiled
return nil return nil
} }

View file

@ -320,3 +320,37 @@ func TestShellTool_RestrictToWorkspace(t *testing.T) {
) )
} }
} }
func TestParsePatternLines(t *testing.T) {
input := `
# comment
\brm\s+-[rf]{1,2}\b
# another comment
\bsource\s+\S+
`
got := parsePatternLines(input)
if len(got) != 2 {
t.Fatalf("expected 2 patterns, got %d: %#v", len(got), got)
}
if got[0] != `\brm\s+-[rf]{1,2}\b` {
t.Fatalf("unexpected first pattern: %q", got[0])
}
if got[1] != `\bsource\s+\S+` {
t.Fatalf("unexpected second pattern: %q", got[1])
}
}
func TestCompileRegexPatterns(t *testing.T) {
compiled, err := compileRegexPatterns([]string{`\brm\s+-[rf]{1,2}\b`, `\bsource\s+\S+`})
if err != nil {
t.Fatalf("expected compile success, got error: %v", err)
}
if len(compiled) != 2 {
t.Fatalf("expected 2 compiled regexes, got %d", len(compiled))
}
if _, err := compileRegexPatterns([]string{`[`}); err == nil {
t.Fatalf("expected invalid regex error, got nil")
}
}