exec.Command默认不经过shell,直接fork+exec二进制文件,因此参数不被shell展开,既安全又需手动拆分命令与参数;需shell特性时须显式调用/bin/sh -c并严格防注入。

exec.Command 不经过 shell,这是默认行为也是安全前提
Go 的 os/exec 默认不调用 /bin/sh 或任何 shell 解释器,而是直接 fork+exec 二进制文件。这意味着 exec.Command("ls", "-la") 是安全的:参数不会被 shell 展开,没有通配符、管道或重定向生效,自然也规避了绝大多数注入场景。
但这也带来一个常见误解:有人以为 exec.Command("sh", "script.sh") 能运行脚本,结果报错 exec: "script.sh": executable file not found in $PATH。这是因为 Go 把 "script.sh" 当作要执行的「程序名」去 PATH 里找,而不是传给 sh 去解释 —— sh 根本没收到这个参数。
- 正确启动脚本:用
exec.Command("sh", "-c", "exec ./script.sh"),其中-c显式启用 shell 解析 - 若脚本有 shebang(如
#!/usr/bin/env bash)且已chmod +x,可直接exec.Command("./script.sh"),前提是路径明确、当前工作目录正确 - 永远不要拼接字符串构造命令:避免
"sh -c 'ls " + userInput + "'"这类写法
传参必须走参数切片,别碰 -c 字符串拼接
只要用了 -c,就等于把控制权交给了 shell —— 此时所有用户输入若直接插进命令字符串,就是给攻击者开后门。比如 exec.Command("sh", "-c", "grep "+pattern+" file.txt"),当 pattern 是 hello; rm -rf /,后果严重。
安全做法是让 shell 自己解析位置参数,不依赖字符串展开:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 单个参数:
exec.Command("sh", "-c", "exec \"$1\"", "sh", scriptPath)——"sh"占位$0,scriptPath成为$1,引号和变量语法由 shell 处理,但输入内容不参与解析 - 多个参数:
exec.Command("sh", "-c", "exec \"$1\" \"$2\" \"$3\"", "sh", scriptPath, arg1, arg2) - 对 Helm、kubectl 等 CLI 工具,一律拆解:
exec.Command("helm", "install", releaseName, chartPath, "--namespace", ns),不走sh -c
捕获输出和判断失败不能靠 err 就完事
cmd.Run() 或 cmd.Output() 返回的 err 只表示「进程启动失败」(如找不到命令),不代表命令执行成功。比如 exec.Command("sh", "-c", "exit 1") 会返回 nil 的 err,但实际退出码是 1。
必须显式检查退出状态:
- 用
cmd.CombinedOutput()拿输出,再用cmd.Wait()或cmd.Run()的返回值判断是否非零退出 - 更精确的做法:
if err := cmd.Run(); err != nil { if exitErr, ok := err.(*exec.ExitError); ok { fmt.Println("exit code:", exitErr.ExitCode()) } } - 超时控制必须设
cmd.Context(),否则ping或tail -f类命令会永久挂起
交互式命令需手动接管 stdin/stdout/stderr 管道
想跑 bash 或 python -i 这类需要持续读写的命令,不能只靠 Output() —— 它是一次性读完就关闭流。必须显式创建管道并并发处理:
- 调
cmd.StdinPipe()、cmd.StdoutPipe()、cmd.StderrPipe() - 用 goroutine 分别做:
io.Copy(os.Stdin, stdin)、io.Copy(os.Stdout, stdout)、io.Copy(os.Stderr, stderr) - 务必用
sync.WaitGroup等待所有 I/O 流结束,再调cmd.Wait()收尾 - 注意:交互式场景下,
cmd.Start()后必须立刻cmd.Wait()或等所有管道完成,否则子进程可能僵死
真正容易被忽略的是:哪怕只是临时调用 sh -c,只要参数来自外部输入,就必须走位置参数传递;而所有涉及长时间运行或交互的命令,其生命周期管理(启动、I/O、等待、清理)必须手动闭环 —— Go 不会替你猜意图。

















