mirror of
https://github.com/RuoJi6/ARTEX.git
synced 2026-10-27 17:03:49 +08:00
352 lines
8.6 KiB
Go
352 lines
8.6 KiB
Go
package intercept
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"mvdan.cc/sh/v3/syntax"
|
|
)
|
|
|
|
type staticSedSubstitution struct {
|
|
pattern *regexp.Regexp
|
|
replacement string
|
|
global bool
|
|
}
|
|
|
|
// Support a deliberately small sed language: ASCII literal substitutions with
|
|
// an optional dot wildcard, escaped literal dots/delimiters, and the g flag.
|
|
// No subprocess, regex backreferences, shell evaluation, or sed e/w commands.
|
|
func parseStaticSed(program string) ([]staticSedSubstitution, bool) {
|
|
if len(program) > 8192 || !asciiText(program) {
|
|
return nil, false
|
|
}
|
|
var subs []staticSedSubstitution
|
|
for program = strings.TrimSpace(program); program != ""; program = strings.TrimSpace(program) {
|
|
if len(subs) >= 32 || len(program) < 3 || program[0] != 's' || !strings.ContainsRune("/#|@%:", rune(program[1])) {
|
|
return nil, false
|
|
}
|
|
delim := program[1]
|
|
program = program[2:]
|
|
var pattern strings.Builder
|
|
closed := false
|
|
for len(program) > 0 {
|
|
c := program[0]
|
|
program = program[1:]
|
|
if c == delim {
|
|
closed = true
|
|
break
|
|
}
|
|
if c == '\\' {
|
|
if len(program) == 0 || (program[0] != '.' && program[0] != delim) {
|
|
return nil, false
|
|
}
|
|
pattern.WriteString(regexp.QuoteMeta(program[:1]))
|
|
program = program[1:]
|
|
} else if c == '.' {
|
|
pattern.WriteByte('.')
|
|
} else {
|
|
if strings.ContainsRune("*[]^$\\", rune(c)) || c < ' ' {
|
|
return nil, false
|
|
}
|
|
pattern.WriteString(regexp.QuoteMeta(string(c)))
|
|
}
|
|
}
|
|
if !closed || pattern.Len() == 0 || pattern.Len() > 4096 {
|
|
return nil, false
|
|
}
|
|
end := strings.IndexByte(program, delim)
|
|
if end < 0 || end > 4096 {
|
|
return nil, false
|
|
}
|
|
replacement := program[:end]
|
|
program = program[end+1:]
|
|
if strings.ContainsAny(replacement, "\\&\n\r") {
|
|
return nil, false
|
|
}
|
|
global := strings.HasPrefix(program, "g")
|
|
if global {
|
|
program = program[1:]
|
|
}
|
|
program = strings.TrimSpace(program)
|
|
if program != "" {
|
|
if program[0] != ';' {
|
|
return nil, false
|
|
}
|
|
program = program[1:]
|
|
}
|
|
re, err := regexp.Compile(pattern.String())
|
|
if err != nil {
|
|
return nil, false
|
|
}
|
|
subs = append(subs, staticSedSubstitution{re, replacement, global})
|
|
}
|
|
return subs, len(subs) > 0
|
|
}
|
|
|
|
func asciiText(text string) bool {
|
|
for i := 0; i < len(text); i++ {
|
|
if text[i] == 0 || text[i] >= 128 {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func applyStaticSed(content string, subs []staticSedSubstitution) (string, bool) {
|
|
// GNU and BSD sed differ on unterminated input. Don't guess that output.
|
|
if !asciiText(content) || (content != "" && !strings.HasSuffix(content, "\n")) {
|
|
return "", false
|
|
}
|
|
for _, sub := range subs {
|
|
var out strings.Builder
|
|
write := func(s string) bool {
|
|
if out.Len()+len(s) > maxReviewScriptBytes {
|
|
return false
|
|
}
|
|
out.WriteString(s)
|
|
return true
|
|
}
|
|
for _, line := range strings.SplitAfter(content, "\n") {
|
|
for {
|
|
loc := sub.pattern.FindStringIndex(line)
|
|
if loc == nil {
|
|
if !write(line) {
|
|
return "", false
|
|
}
|
|
break
|
|
}
|
|
if !write(line[:loc[0]]) || !write(sub.replacement) {
|
|
return "", false
|
|
}
|
|
line = line[loc[1]:]
|
|
if !sub.global {
|
|
if !write(line) {
|
|
return "", false
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
content = out.String()
|
|
}
|
|
return content, true
|
|
}
|
|
|
|
func generatedSedSnapshot(stmt *syntax.Stmt, args []*syntax.Word, dir, target string, generated map[string]scriptReference) (string, string, bool) {
|
|
if len(args) != 3 {
|
|
return "", "", false
|
|
}
|
|
name, known := staticCommandName(args[0])
|
|
if !known || (name != "sed" && name != "/bin/sed" && name != "/usr/bin/sed") {
|
|
return "", "", false
|
|
}
|
|
executable, err := exec.LookPath(name)
|
|
if err != nil {
|
|
return "", "", false
|
|
}
|
|
executable, err = filepath.EvalSymlinks(executable)
|
|
if err != nil || (executable != "/bin/sed" && executable != "/usr/bin/sed") || !nativeInterpreterFile(executable) {
|
|
return "", "", false
|
|
}
|
|
program, known := staticWord(args[1])
|
|
if !known {
|
|
return "", "", false
|
|
}
|
|
subs, known := parseStaticSed(program)
|
|
if !known {
|
|
return "", "", false
|
|
}
|
|
source, known := staticWord(args[2])
|
|
if !known || source == "" || strings.HasPrefix(source, "-") {
|
|
return "", "", false
|
|
}
|
|
if !filepath.IsAbs(source) {
|
|
source = filepath.Join(dir, source)
|
|
}
|
|
source = filepath.Clean(source)
|
|
// Shell redirection truncates before sed opens its input. Never use the old
|
|
// bytes when input/output refer to the same file or symlink/hardlink aliases.
|
|
if source == target {
|
|
return "", "", false
|
|
}
|
|
a, ae := os.Stat(source)
|
|
b, be := os.Stat(target)
|
|
if ae == nil && be == nil && os.SameFile(a, b) {
|
|
return "", "", false
|
|
}
|
|
for _, r := range stmt.Redirs {
|
|
if r.Op == syntax.DplOut || r.Op == syntax.RdrAll {
|
|
return "", "", false
|
|
}
|
|
}
|
|
var content string
|
|
if ref, ok := generated[source]; ok {
|
|
if !ref.inline || ref.problem != "" {
|
|
return "", "", false
|
|
}
|
|
content = ref.content
|
|
} else {
|
|
if !filepath.IsAbs(source) {
|
|
return "", "", false
|
|
}
|
|
raw, err := readScript(source)
|
|
if err != nil {
|
|
return "", "固定替换的原脚本无法读取:" + err.Error(), false
|
|
}
|
|
content = string(raw)
|
|
}
|
|
result, known := applyStaticSed(content, subs)
|
|
return result, "", known
|
|
}
|
|
|
|
func staticSedCommandUnshadowed(tree *syntax.File) bool {
|
|
safe := true
|
|
syntax.Walk(tree, func(node syntax.Node) bool {
|
|
switch n := node.(type) {
|
|
case *syntax.Assign:
|
|
if n.Name != nil && n.Name.Value == "PATH" {
|
|
safe = false
|
|
}
|
|
case *syntax.FuncDecl:
|
|
if n.Name.Value == "sed" {
|
|
safe = false
|
|
}
|
|
case *syntax.CallExpr:
|
|
if len(n.Args) == 0 {
|
|
break
|
|
}
|
|
for _, word := range n.Args {
|
|
if value, known := staticWord(word); known && (strings.HasPrefix(value, "PATH=") || filepath.Base(value) == "sudo") {
|
|
safe = false
|
|
}
|
|
}
|
|
_, name, known := unwrapScriptCall(n.Args)
|
|
if !known {
|
|
safe = false
|
|
break
|
|
}
|
|
switch name {
|
|
case "eval", "alias", "source", ".":
|
|
safe = false
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
return safe
|
|
}
|
|
|
|
// A pre-existing input cannot be reused after an opaque predecessor which may
|
|
// have rewritten it or changed the shell's directory/tool lookup. Known writes
|
|
// from cat/sed remain represented by the virtual-file map; everything else is
|
|
// deliberately excluded instead of treating stale disk bytes as future output.
|
|
func staticSedPrefixKnown(tree *syntax.File, target *syntax.Stmt, remaining *int) bool {
|
|
if *remaining <= 0 {
|
|
return false
|
|
}
|
|
safe := true
|
|
functions := map[string]bool{}
|
|
syntax.Walk(tree, func(node syntax.Node) bool {
|
|
if node == nil {
|
|
return true
|
|
}
|
|
if *remaining <= 0 {
|
|
safe = false
|
|
return false
|
|
}
|
|
*remaining--
|
|
if !safe || node.Pos().After(target.Pos()) {
|
|
return false
|
|
}
|
|
switch n := node.(type) {
|
|
case *syntax.FuncDecl:
|
|
functions[n.Name.Value] = true
|
|
return false
|
|
case *syntax.Assign:
|
|
if n.Name != nil && (n.Name.Value == "PATH" || n.Name.Value == "PWD" || n.Name.Value == "OLDPWD") {
|
|
safe = false
|
|
}
|
|
case *syntax.ForClause, *syntax.IfClause, *syntax.CaseClause, *syntax.ArithmExp, *syntax.ArithmCmd:
|
|
safe = false
|
|
case *syntax.Stmt:
|
|
if n == target {
|
|
return false
|
|
}
|
|
if n.Background {
|
|
safe = false
|
|
return false
|
|
}
|
|
call, ok := n.Cmd.(*syntax.CallExpr)
|
|
if !ok {
|
|
break
|
|
}
|
|
if len(call.Args) == 0 {
|
|
break
|
|
}
|
|
name, known := staticCommandName(call.Args[0])
|
|
if !known || functions[name] {
|
|
safe = false
|
|
break
|
|
}
|
|
base := filepath.Base(name)
|
|
switch base {
|
|
case "cd", "echo":
|
|
if name != base {
|
|
safe = false
|
|
}
|
|
case "printf":
|
|
if name != base || len(call.Args) < 2 {
|
|
safe = false
|
|
break
|
|
}
|
|
format, known := staticWord(call.Args[1])
|
|
if !known || strings.HasPrefix(format, "-v") {
|
|
safe = false
|
|
}
|
|
case "cat", "mkdir", "chmod", "sed":
|
|
executable, err := exec.LookPath(name)
|
|
if err != nil {
|
|
safe = false
|
|
break
|
|
}
|
|
executable, err = filepath.EvalSymlinks(executable)
|
|
if err != nil || (executable != "/bin/"+base && executable != "/usr/bin/"+base) || !nativeInterpreterFile(executable) {
|
|
safe = false
|
|
break
|
|
}
|
|
if base == "sed" {
|
|
if len(call.Args) != 3 || len(n.Redirs) == 0 {
|
|
safe = false
|
|
break
|
|
}
|
|
program, known := staticWord(call.Args[1])
|
|
if !known {
|
|
safe = false
|
|
break
|
|
}
|
|
if _, known := parseStaticSed(program); !known {
|
|
safe = false
|
|
}
|
|
}
|
|
default:
|
|
safe = false
|
|
}
|
|
for _, r := range n.Redirs {
|
|
switch r.Op {
|
|
case syntax.RdrOut, syntax.ClbOut, syntax.AppOut:
|
|
_, known := staticWord(r.Word)
|
|
if !known || (r.N != nil && r.N.Value != "1") {
|
|
safe = false
|
|
}
|
|
case syntax.RdrAll, syntax.DplOut:
|
|
safe = false
|
|
}
|
|
}
|
|
}
|
|
return safe
|
|
})
|
|
return safe
|
|
}
|