文章总结: 慢雾安全团队分析发现GrokCLI0.2.98版本存在严重数据泄露风险。每次对话开始时,CLI会通过gitbundle将整个代码库(包括.gitignore排除的.env、.envrc、config.secret等敏感文件)以明文形式上传至xAI云端存储。上传是独立于模型调用的前置步骤,且脱敏系统仅作用于trace日志,不覆盖gitbundle路径。服务端可通过远程配置强制开启上传,用户本地关闭无效。建议用户谨慎使用或等待官方修复。 综合评分: 88 文章分类: 漏洞分析,数据泄露,安全工具,渗透测试,红队
Grok CLI 风险分析:一个 prompt 如何把敏感文件送上云端?
原创
慢雾安全团队 慢雾安全团队
慢雾科技
2026年7月16日 19:01 中国香港
在小说阅读器读本章
去阅读
**## 事件背景
**##
事情的起因是山哥(23pds,慢雾 CISO)从社区看到一个说法:Grok CLI 可能有风险。具体是什么风险、风险多大,社区里众说纷纭。我(Thinking,慢雾业务安全负责人)手头刚好有新版 0.2.98 的 macOS aarch64 二进制(SHA-256 d5952131…),于是决定自己动手分析一遍——与其猜测,不如把二进制拆开、把流量抓下来、把上传内容恢复出来看个究竟。
分析下来,结论比社区传言更具体:Grok CLI 在每次 turn 开始时,会通过 git bundle 把整个代码库——包括 .gitignore 排除的 .env、.envrc、config.secret——逐字无脱敏地上传到 xAI 的云端存储。上传发生在模型收到推理请求之前,与「Improve the model」开关完全独立,且服务端可通过远程设置强制开启。我用 IDA Pro 9.3 做静态反编译、Frida 做运行时字节验证、mitmproxy 12.2.3 做流量捕获,三条路径交叉验证,并和社区已分析的 0.2.93 做了版本对比——上传机制两版本完全一致。
由于 Grok CLI 没有开源代码,在没有源码的情况下通过反编译和动态调试可以获得一些信息,但是很难保证这些信息的 100% 准确,在这次分析中用了静态分析和动态调试,以及流量分析进行了交叉验证,尽可能让结论不会偏离太多,以下为本文复现的情况。
(注:本文分析完成时,Grok CLI 尚未开源。2026 年 7 月 16 日,Grok CLI 已正式开源,后续我们将结合公开源码进一步验证并补充本文分析结果。)## 复现环境
**##
我搭了一个最小化的测试项目,专门用来验证上传行为:**
/tmp/grok-test-project/├── .env ← 6 个假 API 密钥├── .envrc ← 2 个密钥├── config.secret ← RSA 私钥├── .gitignore ← 排除 .env, .envrc, *.secret├── src/main.py ← print("hello")└── README.md ← # Test Project
**.env 里放了 6 个假的 API 密钥(OpenAI、Anthropic、数据库、AWS、Stripe、JWT),.envrc 放了 2 个,config.secret 放了一段假的 RSA 私钥。.gitignore 明确排除了 .env、.envrc 和 *.secret——这是关键,因为如果 Grok CLI 遵守 .gitignore,这三个文件就不该被上传。
Grok 的配置(~/.grok/config.toml)我用默认条件下观察完整上传行为:**
[features]telemetry = false -> 默认配置
[telemetry]trace_upload = false -> 默认配置
[harness]disable_codebase_upload = false -> 默认配置, 2026.07.13 之后服务端返回的配置变成了 true,这个开关是服务端是可以控制的!
mitmproxy 这边,Grok CLI 用的是 rustls 0.23.37(静态编译 webpki-roots),不走 macOS 系统钥匙串,也没有证书固定(搜 cert.*pin|pinned 全无结果)。于是只要把 SSL_CERT_FILE 指向 mitmproxy 的 CA 证书,所有 HTTPS 流量都能解密:
export GROK_DEPLOYMENT_KEY="fake-deployment-key-for-testing"export GROK_RESPECT_GITIGNORE=0export GROK_SANDBOX=noneexport HTTPS_PROXY="http://127.0.0.1:8080"export SSL_CERT_FILE="$HOME/.mitmproxy/mitmproxy-ca-cert.pem"
**发送的 prompt 很简单:Hello World!。
prompt 发出去后,5.2 秒内 mitmproxy 捕获了 45 个 HTTP 请求。其中 14 个是 POST /v1/storage——上传请求。
最值得关注的是时间线。上传不是模型调用的副产品,而是先于模型调用的前置步骤:
18:31:29.882 POST /v1/storage config.json (4,086B) ← 上传开始18:31:29.883 POST /v1/storage config_files.json (1,485B)18:31:29.895 POST /v1/storage plugins.json (42B)18:31:29.896 POST /v1/storage tool_definitions.json (45,440B)18:31:29.898 POST /v1/storage metadata.json (913B)18:31:29.901 POST /v1/storage before_session_state.tar.gz (4,689B)18:31:29.905 POST api.x.ai/v1/responses (1,252B) ← 模型调用 (晚 4ms)18:31:30.179 ← 模型返回 403 (无 credits)18:31:30.217 ← 上传返回 401 (伪造 key)18:31:30.458 POST /v1/storage git bundle (1,120B) ← 代码库上传18:31:33.815 POST /v1/traces (18,272B) ← OpenTelemetry trace
**6 个初始上传在 19 毫秒内完成(29.882 → 29.901),然后模型调用才开始(29.905)。上传比模型调用早 4 毫秒。换句话说,用户的代码被发往服务端之后,模型才收到推理请求——上传是一个独立的前置步骤,不是模型调用的副作用。
更奇怪的是,即使模型返回 403(账户无 credits),上传请求照样发;即使上传返回 401(我用的是伪造的 deployment key),上传队列仍然持续尝试,直到断路器介入:****
Upload queue circuit breaker tripped, pausing dispatch
## 复现要点
****##
从流量里恢复出敏
感文件,定位 bundle:在捕获到的 14 个上传请求中找 Content-Type: application/gzip 且 x-storage-path 含 repo_changes_dedup/v2/bundles/*.bundle 的请求。
提取与克隆:
cp captured/requests/req_036_*.raw.bin /tmp/captured.bundlegit bundle verify /tmp/captured.bundlegit clone /tmp/captured.bundle grok_bundle_clone
**git bundle verify 输出 The bundle contains 1 ref: refs/heads/main 即为有效 bundle,可直接克隆恢复完整仓库。
这是整个分析里我最想让大家亲眼看到的一步。
我在 14 个上传请求里发现 Content-Type 是 application/gzip,x-storage-path 指向 turn_0/repo_changes_dedup/v2/bundles/sha256_84aa…bundle。这不是一个文件摘要,而是一个标准的 git bundle——可以通过 git clone 直接恢复出完整的仓库历史。
把它从流量里提取出来,用 git 原生命令验证并克隆:
$ git bundle verify /tmp/captured.bundleThe bundle contains 1 ref: refs/heads/main/tmp/captured.bundle is okay
$ git clone /tmp/captured.bundle grok_bundle_clone
**克隆成功,恢复出 6 个文件:.env、.envrc、config.secret、.gitignore、README.md、src/main.py。其中三个是 .gitignore 明确排除的。
.env 的内容,无脱敏:
OPENAI_API_KEY=sk-test-key-1234567890abcdefANTHROPIC_API_KEY=sk-ant-test-key-abcdef123456DATABASE_URL=postgresql://user:password@localhost:5432/dbAWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEYSTRIPE_SECRET_KEY=sk_live_test_stripe_secret_key_12345JWT_SECRET=my-super-secret-jwt-key-1234567890
****6 个 API 密钥、2 个环境变量密钥、一段 RSA 私钥,全部原样躺在 git bundle 里。脱敏过滤器(如果它生效的话)本该把这些替换成 [REDACTED_SECRET]——但它没有。
脱敏过滤器为什么没拦住?这是一个典型的「一致性缝隙」——声明层(有脱敏系统)和实现层(git bundle 不经过脱敏)不一致。安全机制存在,但没有覆盖全部数据出口。
同一个 sk-test-key-1234567890abcdef,在 trace 日志里是 [REDACTED_SECRET],在 git bundle 里是明文。两条路径,两种待遇。脱敏系统给了一个「我已经处理过敏感数据」的承诺,但 git bundle 路径绕过了这个承诺——缝隙就在这里。
Grok CLI 确实内置了一套脱敏系统。从 IDA 里能提取出完整的 12 种正则(基于 Rust regex::RegexSet):Bearer 令牌、GitHub 令牌、GitLab/Slack 令牌、Stripe/xAI 密钥、AWS 密钥、Google API 密钥、PEM 私钥、JWT、用户主目录路径……覆盖面不算窄,匹配后替换为 [REDACTED_SECRET]。
但这套脱敏只作用于 trace 日志和 JSON 元数据里的键值对。源文件路径是 xai-grok-shell/src/upload/trace.rs。而 git bundle 上传走的是另一条路径——xai-data-collector/src/queue.rs,一个独立的上传队列。
于是出现了一个割裂:同一个 .env 文件里的 sk-test-key-1234567890abcdef,在 trace 日志里会被脱敏成 [REDACTED_SECRET],但在 git bundle 里会原样上传。两条路径,两种待遇。一次 turn 里上传的 12 类内容中,git bundle 是唯一携带原始文件内容的那一类,恰好也是唯一不经过脱敏的那一类。
发现了 8 个独立开关
trace_upload 的 8 个独立启用来源(IDA find_regex 地址 0x1060da2b0):****
telemetry_mode -- 全局 telemetry 模式in_requirement_pin -- 编译期需求标记in_requirement_src -- 源码级需求标记in_env_trace_upload -- GROK_TELEMETRY_TRACE_UPLOADin_env_telemetry_enabled -- GROK_TELEMETRY_ENABLEDin_cfg_telemetry_trace_upload -- 配置 telemetry.trace_uploadin_cfg_features_telemetry -- 配置 features.telemetryin_remote_trace_upload_enabled -- 远程设置覆盖
**配置优先级 env > config > remote,但 in_remote_trace_upload_enabled + has_remote_settings 本身就是独立启用来源——服务端可远程强制开启。
race_upload 的启停不由单一开关控制。IDA 里有一段连续字符串,明确列出了 8 个独立启用来源。
最后一个来源 in_remote_trace_upload_enabled 配合 has_remote_settings,这似乎意味着 xAI 后端可以通过远程配置强制开启上传——即使用户在本地关掉了所有环境变量和配置文件。配置优先级是 env > config > remote,但远程设置本身就是独立的启用来源。
更要紧的是,trace_upload 和界面上那个「Improve the model」开关(coding_data_retention_opt_out)完全独立。关掉「Improve the model」不会禁用 trace_upload。二进制里有一条日志说得很直白:
Telemetry disabled but trace uploads enabled: session artifacts will be uploaded, analytics events will not
**respect_gitignore 默认 false
.gitignore 排除的文件为什么还是被上传了?因为 respect_gitignore 的默认值是 false。
IDA 反汇编 0.2.98 的配置解析函数,地址 0x10372EB5C:
loc_10372EB5C: MOV W27, #0 ; respect_gitignore 默认值 = 0 (false)0x10372EBAC: AND W8, W27, #1 ; 提取布尔位0x10372EBB0: STRB W8, [X19, #0xC8] ; 写入结构体偏移 0xC80x10372EBC4: ADRL X0, aGrokRespectGit ; 加载 "GROK_RESPECT_GITIGNORE"
**
MOV W27, #0 把默认值设为 0(false)。为了排除 IDA 分析误差,我用 Frida spawn 进程后直接读运行时内存:**
var base = Process.enumerateModules() .find(m => m.name === "grok").base;// base = 0x100f0c000, ASLR slide = 0xf0c000// IDA 地址 0x10372EBB0 + slide = 0x10463abb0var bytes = Memory.readByteArray(ptr("0x10463abb0"), 4);// 返回: 68 22 03 39 (little-endian)
**读到的字节是 68 22 03 39,little-endian 解码为 0x39032268,即 ARM64 指令 STRB W8, [X19, #0xC8]——和 IDA 反汇编完全一致。二进制里的帮助文本也写明了:respect_gitignore = false # default: false。
默认 false 意味着 Grok CLI 在创建 git bundle 时不会遵守 .gitignore。这就是 .env、.envrc、config.secret 出现在上传内容里的直接原因。
(我在 0.2.93 上也做了同样的验证)
**## 版本对比
社区白帽子 cereblab 之前在 Gist 上分析了 0.2.93,列了 10 项声明。我逐项在 0.2.98 上做了验证。
通过 IDA find_regex 搜 0.2.98、strings | grep 搜 0.2.93,以下方面两版本完全一致:8 个 trace_upload 来源、12 种脱敏正则、8 个存储端点、断路器的状态、GCS 存储桶 grok-code-session-traces、GROK_TELEMETRY_GCS_BUCKET 环境变量、无证书绑定。
(顺带一提,mitmproxy 还抓到 0.2.98 运行时会自动检查 x.ai/cli/stable 获取最新版本号,发现 0.2.101 后自动分 7 片下载新版本二进制。自动更新本身不意外,但它意味着即使你今天分析透了手里这个版本,明天它可能就换了,自动更新可能会引入新版本的功能,但是也会有新的风险。)
**## 缓解措施
由于 in_remote_trace_upload_enabled 的存在,服务端可远程下发配置覆盖本地设置,单一防护层不足以完全禁用上传。建议至少三层并用:
-
环境变量层:GROK_TELEMETRY_ENABLED=0 + GROK_TELEMETRY_TRACE_UPLOAD=0 + GROK_RESPECT_GITIGNORE=1
-
配置文件层:~/.grok/config.toml 中 disable_codebase_upload=true + telemetry=false
-
网络层:防火墙阻断 cli-chat-proxy.grok.com 与 storage.googleapis.com 出站
-
另外:敏感文件移出项目目录;chmod 600 ~/.grok/auth.json。
如果要继续用 Grok CLI 同时减少数据上传,建议三层并用——因为远程设置可覆盖本地配置,单一防护层不够。具体配置见左侧 note。## 写在最后
这次分析让我比较在意的,这不是简单的漏洞,而是「一致性缝隙」——脱敏系统存在但不覆盖 git bundle、8 个开关里有一个是远程可控、「Improve the model」关了但 trace_upload 照传。每一处单独看都有解释(脱敏是给 trace 用的、远程设置是为了运营、「Improve the model」管的是另一回事),但拼在一起,用户的 .env等敏感数据就这么被上传到了云端。
CLI 编码工具的隐私边界,本质上是信任边界。你把整个仓库交给一个会在 turn 开始时就打包上传的二进制,等于把信任交给了它的每一个开关、每一条上传路径、每一次远程配置下发。而信任该持续验证,不该一次性授予。大家也多注意安全。
本文基于 IDA Pro 9.3 反编译、Frida 动态插桩、mitmproxy 12.2.3 流量捕获的交叉验证。分析的二进制版本为 grok-0.2.98 和 grok-0.2.93(macOS aarch64)。
参考资源
cereblab 的 0.2.93 分析:https://gist.github.com/cereblab/dc9a40bc26120f4540e4e09b75ffb547**
往期回顾
TG 账号失守、钱包被调包,macOS 木马如何突破防线?
威胁情报 | Injective SDK 投毒,加密钱包私钥失窃
Google Sites 社群申请钓鱼与 macOS 窃密木马分析
链上 AI 情报官 TrackAgent 正式接入 SlowMist 丢币公益评估
慢雾招募令 | AML 产品 GTM 市场负责人
慢雾导航
慢雾科技官网
https://www.slowmist.com/
慢雾区官网
https://slowmist.io/
慢雾 GitHub
https://github.com/slowmist
Telegram
https://t.me/slowmistteam
https://twitter.com/@slowmist_team
Medium
https://medium.com/@slowmist
知识星球
https://t.zsxq.com/Q3zNvvF
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:慢雾科技 慢雾安全团队 慢雾安全团队《Grok CLI 风险分析:一个 prompt 如何把敏感文件送上云端?》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论