Skip to content

Windows 平台 exec provider 安全审计误判导致 lark-cli 绑定失败 #1475

Description

@zhaocc2

Bug Report: Windows 平台 exec provider 安全审计误判导致 lark-cli 绑定失败

问题描述

在 Windows 平台上,lark-channel-bridge run 启动时 lark-cli 配置绑定(config bind --source lark-channel)始终失败,报错:

exec provider security audit failed: exec provider command: path "C:\\Users\\<user>\\.lark-channel\\secrets-getter.cmd" is world-writable (mode 0666)

Bridge 消息收发不受影响(✓ 已连接),但 agent 无法通过 lark-cli 执行飞书 API 操作(创建文档、搜索联系人等)。

环境信息

  • 操作系统: Windows 11 Home 10.0.26200
  • Node.js: v24.14.0
  • lark-channel-bridge: 0.3.0
  • lark-cli: 1.0.53
  • 安装方式: npm 全局安装(npm i -g lark-channel-bridge

复现步骤

  1. 在 Windows 上全局安装 lark-channel-bridge
  2. 完成初始化(创建 profile、绑定应用)
  3. 执行 lark-channel-bridge run
  4. 观察输出中 "lark-cli configuration failed" 及完整错误信息

根因分析

核心矛盾

lark-cli.exe(Go 编译)对 exec provider 的 command 路径(secrets-getter.cmd)做安全审计时,使用文件的 Unix mode 判断是否 "world-writable"。但在 Windows/NTFS 上:

  • Go 的 os.Stat()Node.js 的 fs.stat() 对非只读文件一律返回 mode 0666
  • 对只读文件返回 mode 0444
  • Windows 不使用 Unix 权限模型,这两个值是通过"只读属性标志"推断的,与 NTFS ACL 无关

死循环

文件状态 Node.js/Go 报告的 mode bridge 的 rename 操作 lark-cli 安全审计
非只读 0666 ✅ 成功 ❌ 拒绝("world-writable")
只读 0444 ❌ 失败(EPERM) ✅ 通过
  • 如果文件非只读:lark-cli 报 "world-writable" 拒绝执行
  • 如果文件只读:bridge 的 writeFileAtomic() 在 rename 时报 EPERM 无法更新文件

验证

# Git Bash 中显示 644(因为 icacls /reset 修改了 ACL)
$ stat -c '%a' ~/.lark-channel/secrets-getter.cmd
644

# 但 Node.js 仍然返回 0666(只看只读标志,不看 ACL)
$ node -e "const s=require('fs').statSync(process.env.USERPROFILE+'/.lark-channel/secrets-getter.cmd'); console.log('0'+((s.mode)&0o777).toString(8))"
0666

相关代码位置

  1. bridge 写入 secrets-getter.cmd:

    • src/platform/atomic-write.tswriteFileAtomic(wrapperPath, content, { mode: 0o600 })
    • Windows 上 chmod(tmp, 0o600) 实际效果等同于不设只读 → mode 显示为 0666
  2. lark-cli 安全审计:

    • lark-cli.exe 内部的 AssertSecurePath 逻辑
    • 使用 Go 的 os.Stat().Mode() 检查路径是否 world-writable
  3. bridge 绑定流程:

    • bindLarkCliWithCompatibility() → 执行 lark-cli config bind --source lark-channel
    • lark-cli 读取 lark-cli-source/config.json 中的 exec provider 配置
    • 调用 secrets-getter.cmd 前触发安全审计 → 失败

建议修复方案

方案 A:lark-cli 侧(推荐)

AssertSecurePath 中对 Windows 平台跳过 Unix mode 检查,改为检查 NTFS ACL(如使用 Go 的 windows.GetSecurityInfo API),或直接在 runtime.GOOS == "windows" 时跳过 mode 校验。

方案 B:bridge 侧

在 Windows 上写入 secrets-getter.cmd 后设置只读属性,并在需要更新时先移除只读再 rename:

// 写入后设为只读(通过 Windows API 或 attrib +R)
if (process.platform === 'win32') {
  await exec(`attrib +R "${wrapperPath}"`);
}

// 更新前移除只读
if (process.platform === 'win32') {
  await exec(`attrib -R "${targetPath}"`);
}
await rename(tmp, targetPath);
if (process.platform === 'win32') {
  await exec(`attrib +R "${targetPath}"`);
}

方案 C:环境变量

lark-cli 提供环境变量(如 LARK_CLI_SKIP_EXEC_AUDIT=1)允许用户在受信任环境中跳过安全审计。

当前 Workaround

  1. 使用 --skip-check-lark-cli 启动 bridge(跳过自动绑定)
  2. 手动将 lark-cli-source/config.json 中的 exec provider 替换为明文 secret
  3. 手动执行 lark-cli config bind --source lark-channel --identity user-default
  4. 手动完成用户授权(lark-cli auth login --recommend

Issue 模板(可直接提交到 GitHub)

### Bug: Windows 上 exec provider 安全审计误判 "world-writable (mode 0666)" 导致 lark-cli 绑定失败

**环境**
- OS: Windows 11 (10.0.26200)
- Node.js: v24.14.0
- lark-channel-bridge: 0.3.0
- lark-cli: 1.0.53

**现象**
`lark-channel-bridge run` 启动后 lark-cli configuration 始终失败:
\```
failed to resolve appSecret for cli_xxx: exec provider security audit failed:
exec provider command: path "C:\\Users\\<user>\\.lark-channel\\secrets-getter.cmd"
is world-writable (mode 0666)
\```

**根因**
Go/Node.js 的 `stat()` 在 Windows 上对非只读文件返回 `mode 0666`(通过只读属性标志推断,与实际 NTFS ACL 无关)。lark-cli 的 `AssertSecurePath` 将此判定为 "world-writable" 并拒绝执行 exec provider。

而文件若设为只读,bridge 的 atomic-write rename 又会因 EPERM 失败,形成死循环。

**复现**
1. Windows 上 `npm i -g lark-channel-bridge`
2. 完成 profile 初始化
3. `lark-channel-bridge run` → 观察 lark-cli configuration failed

**期望**
Windows 上安全审计不应使用 Unix file mode 来判断文件是否 world-writable,或提供平台特定的替代检查方式。

**Workaround**
使用 `--skip-check-lark-cli` 启动 + 手动将 source config 改为明文 secret + 手动执行 `lark-cli config bind`

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain/authAuthentication subsystemdomain/coreCLI framework and core librariesdomain/imPR touches the im domain

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions