package permission // sed 命令安全验证. // // sed 的 e 标志可以执行任意 shell 命令,w 标志可以写入任意文件. // 这两个是 sed 最危险的能力,必须检测并阻止. // // 安全模式分两种: // Pattern 1: 只读打印(sed -n '1,5p')→ 安全 // Pattern 2: 替换(sed 's/old/new/g')→ 有条件安全 // // 精妙之处(CLEVER): 移植早期设计中的 sed 安全检查逻辑,但用 AST 感知的分词替代 // 早期方案的字符串分割.早期方案在遇到 sed -e 'expr1' -e 'expr2' 时可能误分割 // 引号内的空格,我们通过 tokenizeCommand 正确处理. import ( "strings" "unicode" "unicode/utf8" ) // SedCheckResult 是 sed 命令安全检查的结果. type SedCheckResult struct { Safe bool // 是否安全 Reason string // 原因说明 Pattern string // 模式类型:"print" / "substitution" / "dangerous" / "unknown" } // CheckSedCommand 检查 sed 命令是否安全. // // 检查流程: // 1. 提取 sed 表达式(-e,--expression=,内联表达式) // 2. 检查每个表达式是否包含危险操作 // 3. 如果全部安全,判断是打印模式还是替换模式 // // allowFileWrites: 是否允许文件写入(w 标志).一般为 false. func CheckSedCommand(command string, allowFileWrites bool) SedCheckResult { tokens := tokenizeCommand(command) if len(tokens) == 0 { return SedCheckResult{Safe: false, Reason: "empty command", Pattern: "unknown"} } // 跳过 sed 命令名本身(可能是路径如 /usr/bin/sed) cmdIdx := 0 for cmdIdx < len(tokens) { base := tokens[cmdIdx] if idx := strings.LastIndex(base, "/"); idx >= 0 { base = base[idx+1:] } if base == "sed" || base == "gsed" { cmdIdx++ break } cmdIdx++ } if cmdIdx >= len(tokens) { // 没找到 sed 命令名,当作不安全 return SedCheckResult{Safe: false, Reason: "cannot find sed command", Pattern: "unknown"} } // 提取选项和表达式 expressions, hasNFlag, err := extractSedExpressions(tokens[cmdIdx:]) if err != "" { return SedCheckResult{Safe: false, Reason: err, Pattern: "unknown"} } if len(expressions) == 0 { return SedCheckResult{Safe: false, Reason: "no sed expressions found", Pattern: "unknown"} } // 检查每个表达式是否包含危险操作 for _, expr := range expressions { if reason := containsDangerousOperations(expr, allowFileWrites); reason != "" { return SedCheckResult{Safe: false, Reason: reason, Pattern: "dangerous"} } } // 判断模式类型 if hasNFlag && allLinePrinting(expressions) { return SedCheckResult{Safe: true, Reason: "safe line printing command", Pattern: "print"} } if allSubstitution(expressions) { return SedCheckResult{Safe: true, Reason: "safe substitution command", Pattern: "substitution"} } // 无法识别的模式,但没有检测到危险操作 // 保守处理:仍然不安全 return SedCheckResult{Safe: false, Reason: "unrecognized sed pattern", Pattern: "unknown"} } // extractSedExpressions 从 sed 命令参数中提取所有表达式. // // 处理方式: // - -e expr 或 -eexpr → 提取 expr // - --expression=expr → 提取 expr // - -n, -i, -r, -E 等 → 识别为选项 // - 非选项的第一个参数 → 内联表达式 // - 后续非选项参数 → 文件参数(忽略) // // 返回 (表达式列表, 是否有 -n 标志, 错误信息). func extractSedExpressions(args []string) ([]string, bool, string) { var expressions []string hasNFlag := false foundInlineExpr := false i := 0 for i < len(args) { arg := args[i] if arg == "--" { // -- 后面全是文件参数 break } // --expression=value if strings.HasPrefix(arg, "--expression=") { expressions = append(expressions, arg[len("--expression="):]) i++ continue } // 长选项 if strings.HasPrefix(arg, "--") { // --in-place, --regexp-extended 等,跳过 i++ continue } // 短选项 if strings.HasPrefix(arg, "-") && len(arg) > 1 { // 解析融合短选项如 -ne, -ni 等 j := 1 for j < len(arg) { ch := arg[j] switch ch { case 'n': hasNFlag = true j++ case 'e': // -e 后面的部分或下一个参数是表达式 rest := arg[j+1:] if rest != "" { expressions = append(expressions, rest) } else { i++ if i >= len(args) { return nil, false, "-e requires an argument" } expressions = append(expressions, args[i]) } j = len(arg) // 跳出内层循环 case 'f': // -f file,跳过文件参数 if j+1 < len(arg) { // 融合形式 -fscript.sed } else { i++ // 跳过下一个参数 } j = len(arg) case 'i': // -i[suffix],可能带也可能不带后缀 // -i'' 或 -i 后跟其他选项或参数 j = len(arg) // 消费剩余部分作为后缀 case 'r', 'E', 'l', 'u', 'z', 's': // 其他已知选项 j++ default: j++ } } i++ continue } // 非选项参数 if !foundInlineExpr && len(expressions) == 0 { // 第一个非选项参数是内联表达式 expressions = append(expressions, arg) foundInlineExpr = true } // 后续非选项参数是文件名,忽略 i++ } return expressions, hasNFlag, "" } // containsDangerousOperations 检查单个 sed 表达式是否包含危险操作. // // 危险操作列表: // - e/E 标志(执行 shell 命令) // - w/W 标志(写入文件) // - 独立 e 命令(/pattern/e cmd) // - 独立 w 命令(/pattern/w file) // - 非 ASCII 字符(Unicode 同形攻击) // - 花括号(复杂块,难以静态分析) // - 反斜杠分隔符(s\old\new\,混淆分析) // // 返回空字符串表示安全,否则返回危险原因. func containsDangerousOperations(expr string, allowFileWrites bool) string { if expr == "" { return "" } // 精妙之处(CLEVER): 非 ASCII 字符检测防御 Unicode 同形攻击. // 攻击者可以用 Cyrillic 字母 'е'(U+0435) 替代 Latin 'e', // 让 sed 表达式在视觉上无害,实际执行 e 命令. // 我们直接拒绝所有非 ASCII,一刀切但绝对安全. if containsNonASCII(expr) { return "expression contains non-ASCII characters (potential homograph attack)" } // 花括号检测--花括号内可以包含任意 sed 命令序列, // 静态分析花括号内容需要完整的 sed 语法解析器,不值得. if strings.ContainsAny(expr, "{}") { return "expression contains curly braces (complex block, cannot statically verify)" } // 检查 s 命令的替换表达式 if isSedSubstitution(expr) { return checkSubstitutionFlags(expr, allowFileWrites) } // 检查独立的 e 命令(/pattern/e 或 e cmd) stripped := stripSedAddress(expr) if stripped == "e" || strings.HasPrefix(stripped, "e ") || strings.HasPrefix(stripped, "e\t") { return "expression contains 'e' command (executes shell commands)" } // 检查独立的 w 命令 if !allowFileWrites { if stripped == "w" || strings.HasPrefix(stripped, "w ") || strings.HasPrefix(stripped, "w\t") || stripped == "W" || strings.HasPrefix(stripped, "W ") || strings.HasPrefix(stripped, "W\t") { return "expression contains 'w'/'W' command (writes to file)" } } // 检查 R/r 命令(读取文件--虽然不直接危险,但可能被用于信息泄露) // 不阻止,只是标注.安全的打印和替换模式会额外验证. return "" } // isSedSubstitution 判断表达式是否是 s 替换命令. func isSedSubstitution(expr string) bool { stripped := stripSedAddress(expr) if stripped == "" { return false } return stripped[0] == 's' && len(stripped) > 1 } // checkSubstitutionFlags 检查 s 替换命令的标志是否安全. // // 允许的标志:g, p, i, I, m, M, 1-9(出现次数) // 危险标志:e/E(执行替换结果为 shell 命令), w/W(写入文件) func checkSubstitutionFlags(expr string, allowFileWrites bool) string { stripped := stripSedAddress(expr) if len(stripped) < 2 || stripped[0] != 's' { return "" } delim := stripped[1] // 精妙之处(CLEVER): 反斜杠分隔符拒绝. // sed 允许 s\old\new\ 形式,但反斜杠在 sed 中同时是转义字符, // 解析 s\o\d\new\ 时很容易出错,可能绕过安全检查. // 直接拒绝,正常人不会用反斜杠做分隔符. if delim == '\\' { return "backslash delimiter in substitution (ambiguous parsing, rejected)" } // 只允许 / 做分隔符(严格模式) // 历史包袱(LEGACY): sed 其实允许几乎任何字符做分隔符(如 s|old|new|, s#old#new#). // 但允许任意分隔符会大幅增加解析复杂度,且容易被利用来混淆分析. // 严格只认 / 可能误报一些合法用法,但安全优先. if delim != '/' { return "only '/' delimiter is allowed in substitution (security restriction)" } // 找到第三个分隔符后面的标志部分 flags := extractSubstitutionFlags(stripped) if flags == "" { return "" // 没有标志,安全 } // 逐字符检查标志 for _, ch := range flags { switch ch { case 'g', 'p', 'i', 'I', 'm', 'M': // 安全标志 continue case '1', '2', '3', '4', '5', '6', '7', '8', '9': // 出现次数,安全 continue case 'e', 'E': return "substitution contains 'e'/'E' flag (executes result as shell command)" case 'w', 'W': if !allowFileWrites { return "substitution contains 'w'/'W' flag (writes to file)" } default: return "substitution contains unknown flag: " + string(ch) } } return "" } // extractSubstitutionFlags 从 s/pattern/replacement/flags 中提取 flags 部分. func extractSubstitutionFlags(expr string) string { if len(expr) < 2 || expr[0] != 's' { return "" } delim := expr[1] // 跳过 s + delim count := 0 escaped := false for i := 2; i < len(expr); i++ { ch := expr[i] if escaped { escaped = false continue } if ch == '\\' { escaped = true continue } if ch == delim { count++ if count == 2 { // 后面的就是标志 return expr[i+1:] } } } return "" } // stripSedAddress 去除 sed 表达式开头的地址部分(行号,正则地址等). // // 地址形式: // - 数字: 5cmd // - 范围: 1,5cmd // - 正则: /pattern/cmd // - 步进: 0~2cmd func stripSedAddress(expr string) string { if expr == "" { return "" } i := 0 // 跳过地址 i = skipOneAddress(expr, i) // 可能有逗号范围 if i < len(expr) && expr[i] == ',' { i++ i = skipOneAddress(expr, i) } if i >= len(expr) { return "" } return expr[i:] } // skipOneAddress 跳过一个地址. func skipOneAddress(expr string, start int) int { i := start if i >= len(expr) { return i } switch { case expr[i] >= '0' && expr[i] <= '9': // 数字地址 for i < len(expr) && expr[i] >= '0' && expr[i] <= '9' { i++ } // 步进 ~N if i < len(expr) && expr[i] == '~' { i++ for i < len(expr) && expr[i] >= '0' && expr[i] <= '9' { i++ } } case expr[i] == '$': // 末行地址 i++ case expr[i] == '/': // 正则地址 /pattern/ i++ // 跳过开头 / escaped := false for i < len(expr) { if escaped { escaped = false i++ continue } if expr[i] == '\\' { escaped = true i++ continue } if expr[i] == '/' { i++ // 跳过结尾 / break } i++ } case expr[i] == '\\': // 自定义分隔符正则 \cpatternc if i+1 < len(expr) { delim := expr[i+1] i += 2 for i < len(expr) && expr[i] != delim { if expr[i] == '\\' && i+1 < len(expr) { i++ // 跳过转义 } i++ } if i < len(expr) { i++ // 跳过结尾分隔符 } } } return i } // containsNonASCII 检查字符串是否包含非 ASCII 字符. func containsNonASCII(s string) bool { for i := 0; i < len(s); { r, size := utf8.DecodeRuneInString(s[i:]) if r > unicode.MaxASCII { return true } i += size } return false } // isLinePrintingCommand 验证表达式是否是安全的只读打印命令. // // 安全的打印命令形式: // - Np (打印第 N 行) // - N,Mp (打印第 N 到 M 行) // - $p (打印最后一行) func isLinePrintingCommand(expr string) bool { expr = strings.TrimSpace(expr) if expr == "" { return false } // 必须以 p 结尾 if !strings.HasSuffix(expr, "p") { return false } // 去掉尾部 p,剩下的应该是纯地址 addr := expr[:len(expr)-1] if addr == "" { return true // 裸 p 也是安全的(打印当前行) } return isValidPrintAddress(addr) } // isValidPrintAddress 验证地址部分是否是安全的打印地址. func isValidPrintAddress(addr string) bool { // 支持 N, $, N,M, N,$ parts := strings.SplitN(addr, ",", 2) for _, part := range parts { part = strings.TrimSpace(part) if part == "$" { continue } // 纯数字 if isNumeric(part) { continue } return false } return true } // isNumeric 检查字符串是否是纯数字. func isNumeric(s string) bool { if s == "" { return false } for _, ch := range s { if ch < '0' || ch > '9' { return false } } return true } // allLinePrinting 检查所有表达式是否都是安全的打印命令. func allLinePrinting(expressions []string) bool { for _, expr := range expressions { if !isLinePrintingCommand(expr) { return false } } return true } // isSubstitutionCommand 验证表达式是否是安全的替换命令. func isSubstitutionCommand(expr string) bool { stripped := stripSedAddress(expr) if stripped == "" || stripped[0] != 's' { return false } // 检查标志安全性 return checkSubstitutionFlags(expr, false) == "" } // allSubstitution 检查所有表达式是否都是安全的替换命令. func allSubstitution(expressions []string) bool { for _, expr := range expressions { if !isSubstitutionCommand(expr) { return false } } return true }