(expr string, hasInplace bool)
| 73 | } |
| 74 | |
| 75 | func validateSingleSedExpression(expr string, hasInplace bool) SedValidationResult { |
| 76 | if expr == "" { |
| 77 | return SedValidationResult{Safe: false, Reason: "Empty expression"} |
| 78 | } |
| 79 | if !IsASCII(expr) { |
| 80 | return SedValidationResult{Safe: false, Reason: "Expression contains non-ASCII characters"} |
| 81 | } |
| 82 | if strings.HasPrefix(expr, `s\`) { |
| 83 | return SedValidationResult{Safe: false, Reason: "Backslash delimiter in substitute command"} |
| 84 | } |
| 85 | if strings.Contains(expr, "{") || strings.Contains(expr, "}") { |
| 86 | return SedValidationResult{Safe: false, Reason: "Script blocks {} are not auto-approved"} |
| 87 | } |
| 88 | trimmed := strings.TrimSpace(expr) |
| 89 | if strings.Contains(expr, "!") && !strings.HasPrefix(expr, "#!") { |
| 90 | if regexp.MustCompile(`(?:/(?:.*)|\d)! *[a-zA-Z]`).MatchString(expr) || |
| 91 | regexp.MustCompile(`^!\s*[a-zA-Z]`).MatchString(trimmed) { |
| 92 | return SedValidationResult{Safe: false, Reason: "Address negation (!) is not auto-approved"} |
| 93 | } |
| 94 | } |
| 95 | if sedExecuteFlagsRE.MatchString(expr) { |
| 96 | return SedValidationResult{Safe: false, Reason: "Execute flag (e/E) can run arbitrary shell commands"} |
| 97 | } |
| 98 | if sedWriteCommandsRE.MatchString(expr) { |
| 99 | return SedValidationResult{Safe: false, Reason: "Write command (w/W) can write to arbitrary files"} |
| 100 | } |
| 101 | if hasInplace { |
| 102 | if match := regexp.MustCompile(`(?:^|[^a-zA-Z])([daic])\s`).FindStringSubmatch(expr); len(match) > 1 { |
| 103 | return SedValidationResult{Safe: false, Reason: "'" + match[1] + "' command with -i modifies files"} |
| 104 | } |
| 105 | } |
| 106 | if regexp.MustCompile(`^[\d,\s;$]*p[\d;]*$`).MatchString(trimmed) { |
| 107 | return SedValidationResult{Safe: true, Reason: "Safe: line printing only (p command)"} |
| 108 | } |
| 109 | if flags, ok := parseSedSubstituteFlags(trimmed); ok { |
| 110 | if flags == "" || safeSubstituteFlagsRE.MatchString(flags) { |
| 111 | return SedValidationResult{Safe: true, Reason: "Safe: substitution with allowed flags"} |
| 112 | } |
| 113 | } |
| 114 | if strings.Contains(expr, ";") { |
| 115 | parts := splitSedExpressions(expr) |
| 116 | if len(parts) > 1 { |
| 117 | for _, part := range parts { |
| 118 | result := validateSingleSedExpression(strings.TrimSpace(part), hasInplace) |
| 119 | if !result.Safe { |
| 120 | return result |
| 121 | } |
| 122 | } |
| 123 | return SedValidationResult{Safe: true, Reason: "Safe: all sub-expressions passed"} |
| 124 | } |
| 125 | } |
| 126 | return SedValidationResult{Safe: false, Reason: "Expression does not match any safe pattern"} |
| 127 | } |
| 128 | |
| 129 | func looksLikeSedScript(token string) bool { |
| 130 | return regexp.MustCompile(`^[sdyapicq]`).MatchString(token) || |
no test coverage detected