mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-10-03 15:53:56 +00:00
feat(waiter): 设备命令白名单改为 waiter.yaml 可配置
## 起因
白名单是源码里硬编码的正则(`homeagentAllowCmd`,18 个命令),
而 `waiter.yaml` 里**没有任何键能改它** ⇒ `find` / `grep` / `sed` / `sort` / `tr`
这些排查问题最常用的**只读**命令一律被拒。生产实测:
device_ctl_cmdrun device_id:waiter-fnnas error: command not in whitelist
命令执行完全在 waiter 侧(`device.go` 的 `exec.CommandContext`),插件侧无二次
限制;触发者是 **agent**(经 device_ctl_cmdrun),所以这道闸是机器闸、不是人工确认。
## 改动
waiter.yaml 新增 `device_cmd_allowlist`(字符串数组):
device_cmd_allowlist:
- ls
- find
- grep
- sed
- **替换**默认集而非追加:避免"以为加了 find、结果还留着 python3 -c 任意执行"
- 留空 ⇒ 用内置默认集(★ **绝不能变成"全放行"**,那等于静默拆掉闸门)
- 匹配只取命令名**第一段**再整词匹配:`grep -rn x .` 能过,
而 `grepXxx` / `mygrep` 不会因 contains 蒙混过关;也跳过 `FOO=bar cmd` 的赋值前缀
- `deviceCmdAllowed` 是包级函数变量,由配置赋值 —— 与同文件既有的
`sendBridgeResult` 同一模式
## ★ 一次真实的疏漏(判据记着)
waiter 有**两条**设备桥启动路径:
- `main.go` 的 `startDeviceBridge` —— 交互/一次性模式
- `daemon.go` 的 `startDaemonDeviceBridge` —— `waiter --daemon`(**生产两台都这么跑**)
我最初只在 `main.go` 里赋值。daemon 路径不经过那里 ⇒ 配置**完全不生效**,
而症状是"配置写了、启动也打了招呼、命令照样被拒",极难定位。
两处都接上了,并加 `TestDaemonPathAppliesAllowlist` 守住。
## 判据(5 条)
- `TestDefaultAllowlistStillBlocksDestructive` 默认集必须挡住
`rm -rf /`、`dd`、`chmod -R 777`、`mkfs`、fork 炸弹 ——
**这道闸存在的唯一理由**,谁把它改成"什么都不拦"这条就要失败
- `TestConfigAllowlistExtends` 配置里声明的 `find/grep/sed/sort/tr` 能过;
配置未含的 `rm -rf /` 仍被拒(证明是"替换"不是"叠加")
- `TestEmptyConfigFallsBackToDefault` 配置为空时回退默认集,**且不放行** `rm -rf /`
- `TestCmdAllowlistFromYAML` 走**真实** `readFile` 解析 yaml(不另写一份解析,
两处会漂移,而漂移本身就是漏洞)
- `TestDaemonPathAppliesAllowlist` 守住 daemon 路径也应用配置
## 生效方式
106/30 的 `/opt/waiter/waiter.yaml` 追加 `device_cmd_allowlist`,
并更新二进制。启动日志会打印 `device cmd allowlist: N 条(来自 waiter.yaml)`
或 `默认 N 条`,便于确认配置是否真的被读到。
This commit is contained in:
@ -93,10 +93,59 @@ func stopDeviceBridge() {
|
||||
|
||||
// ===== 命令分发 =====
|
||||
|
||||
// homeagent 能力白名单命令(与 remotedevice 插件对齐)
|
||||
var homeagentAllowCmd = regexp.MustCompile(
|
||||
"^(ls|pwd|whoami|uname|date|echo|uptime|hostname|cat|df|free|ps|ip|dir|node|python3?|npm|git|curl|wget|systeminfo|tasklist)\\b",
|
||||
)
|
||||
// defaultCmdAllowlist 是**内置默认**命令白名单(命令名的第一段)。
|
||||
//
|
||||
// 只收「只读/低风险」的诊断类命令。这道闸的唯一作用是挡住
|
||||
// rm -rf /、dd、chmod 777、fork 炸弹这类破坏性命令 —— 而触发它的是
|
||||
// **agent**(经 device_ctl_cmdrun),不是人,所以需要一道机器闸。
|
||||
var defaultCmdAllowlist = []string{
|
||||
"ls", "pwd", "whoami", "uname", "date", "echo", "uptime", "hostname",
|
||||
"cat", "df", "free", "ps", "ip", "dir", "node", "python3", "python",
|
||||
"npm", "git", "curl", "wget", "systeminfo", "tasklist",
|
||||
}
|
||||
|
||||
// buildCmdMatcher 由命令名列表构造匹配函数。
|
||||
//
|
||||
// 只取**命令名的第一段**再整词匹配:这样 "grep -rn x ." 能过,
|
||||
// 而 "grepXxx" / "mygrep" 不会因为 contains 而蒙混过关。
|
||||
// 空列表 ⇒ 回退默认集(★ 绝不能变成"全放行")。
|
||||
func buildCmdMatcher(cmds []string) func(string) bool {
|
||||
if len(cmds) == 0 {
|
||||
cmds = defaultCmdAllowlist
|
||||
}
|
||||
set := make(map[string]bool, len(cmds))
|
||||
for _, c := range cmds {
|
||||
if c = strings.TrimSpace(c); c != "" {
|
||||
set[c] = true
|
||||
}
|
||||
}
|
||||
return func(command string) bool {
|
||||
fields := strings.Fields(strings.TrimSpace(command))
|
||||
if len(fields) == 0 {
|
||||
return false
|
||||
}
|
||||
// 跳过 VAR=value 前缀(`FOO=bar cmd` 这种合法写法)
|
||||
i := 0
|
||||
for i < len(fields) && strings.Contains(fields[i], "=") &&
|
||||
!strings.HasPrefix(fields[i], "-") {
|
||||
i++
|
||||
}
|
||||
if i >= len(fields) {
|
||||
return false
|
||||
}
|
||||
return set[filepath.Base(fields[i])]
|
||||
}
|
||||
}
|
||||
|
||||
// deviceCmdAllowed 是当前生效的命令白名单匹配函数。
|
||||
//
|
||||
// 包级变量 + 由 main 从配置赋值,与同文件既有的 sendBridgeResult 同一模式
|
||||
// (那里也是包级函数变量,测试可替换)。
|
||||
var deviceCmdAllowed = buildCmdMatcher(nil)
|
||||
|
||||
// cmdAllowed / defaultCmdAllowed 是两个测试可读的入口。
|
||||
func cmdAllowed(command string) bool { return deviceCmdAllowed(command) }
|
||||
func defaultCmdAllowed(command string) bool { return buildCmdMatcher(nil)(command) }
|
||||
|
||||
func handleShellCmd(reqID, command string) {
|
||||
cmd := strings.TrimSpace(command)
|
||||
@ -104,7 +153,7 @@ func handleShellCmd(reqID, command string) {
|
||||
sendBridgeResult(reqID, "error", "", "empty command")
|
||||
return
|
||||
}
|
||||
if !homeagentAllowCmd.MatchString(cmd) {
|
||||
if !cmdAllowed(cmd) {
|
||||
sendBridgeResult(reqID, "error", "", "command not in whitelist")
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user