前言:供应链攻击之下,每一行命令都可能是入口
2025 年至今,针对 npm、PyPI、GitHub Actions、Docker Hub 的供应链攻击事件持续走高:被污染的依赖、被劫持的 CI Token、被钓鱼的维护者账号……攻击者越来越偏好「绕过你的代码、直接拿你的凭证」的低成本路径。
而开发者最容易忽略的一处 “凭证仓库” ,恰恰是每天都在用、却几乎从不审计的:Shell 历史记录文件。
- macOS / Linux:
~/.zsh_history、~/.bash_history - Windows PowerShell:
%APPDATA%\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt
这些文件以 明文 保存你输入过的所有命令——包括误粘贴的 API Key、带密码的 URL、订阅链接、数据库连接串。一旦你的电脑被恶意 npm 包、伪装 VSCode 插件、或被入侵的 CLI 工具读取到这些路径,攻击者无需提权就能直接打包带走。
但是有时也的确会需要在命令中输入密钥,比如 export API_KEY=,本文给出两件可立刻落地的小事:
- 写命令时:让敏感命令一开始就不进历史。
- 写完之后:定期扫一遍历史文件,看看自己是不是早就漏过。
一、macOS / Linux:在命令前加一个空格
bash 和 zsh 都支持 HISTCONTROL / HIST_IGNORE_SPACE —— 一旦开启,以空格开头的命令不会被写入历史文件,也不会出现在 ↑ 历史回溯里。
很多发行版或 shell 框架可能已经启用启用了这个特性。验证一下:
1 | # bash - 期望包含 ignorespace 或 ignoreboth |
如果没有,加入你的 ~/.bashrc 或 ~/.zshrc:
1 | # bash |
之后,敏感命令前面加一个空格即可:
1 | export OPENAI_API_KEY="sk-..." |
退后退出本次终端(将命令写入历史记录文件),重新打开,使用下面的命令验证:
1 | tail -n 5 ~/.zsh_history |
1 | tail -n 5 ~/.bash_history |
二、Windows PowerShell:用 AddToHistoryHandler 自己实现
PowerShell 没有内置「前导空格不入历史」的开关,但 PSReadLine 提供了 AddToHistoryHandler,可以自定义哪些命令进历史。
配置步骤
打开(或创建)你的 PowerShell profile:
1 | notepad $PROFILE |
写入:
1 | Set-PSReadLineOption -AddToHistoryHandler { |
保存后,重启 PowerShell。之后像这样以空格开头输入的命令,不会写入当前会话历史,也不会落盘到 ConsoleHost_history.txt:
1 | # ... |
PSReadLine 文档明确说明:handler 返回
$false等同于SkipAdding,当前会话和历史文件都不会保留这条命令。
这只影响之后输入的新命令;已经写进
ConsoleHost_history.txt的内容不会被清理。
三、回头审计:你的历史里早就有什么?
加了开关只是治未病。下一步是翻账——很多人第一次扫描自家 ~/.zsh_history 都会被吓到。
下面这一组命令面向 zsh,bash 用户把路径换成 ~/.bash_history 即可。
1. 先看文件大小,心里有数
1 | ls -lh ~/.zsh_history |
2. 排查常见密钥/密码关键词
1 | grep -nEi 'api[_-]?key|apikey|secret|token|password|passwd|pwd|authorization|bearer|credential|client_secret|access[_-]?key|private[_-]?key|OPENAI_API_KEY|ANTHROPIC_API_KEY|GITHUB_TOKEN|GH_TOKEN|NPM_TOKEN|AWS_ACCESS_KEY_ID|AWS_SECRET_ACCESS_KEY|DATABASE_URL|MONGODB_URI|REDIS_URL' ~/.zsh_history |
3. 排查典型 Token 格式
1 | grep -nE 'sk-[A-Za-z0-9_-]{20,}|github_pat_[A-Za-z0-9_]{20,}|gh[pousr]_[A-Za-z0-9]{20,}|AKIA[0-9A-Z]{16}|AIza[0-9A-Za-z_-]{35}' ~/.zsh_history |
覆盖:OpenAI / Anthropic 风格 sk-…、GitHub Fine-grained PAT、Classic PAT、AWS Access Key、Google API Key。
4. 排查带账号密码的 URL
1 | grep -nEi 'https?://[^[:space:]/@:]+:[^[:space:]/@]+@' ~/.zsh_history |
形如 https://user:password@host/... —— 在 git clone、curl、psql 里非常常见。
5. 排查环境变量赋值
1 | grep -nEi '(\$?[A-Z0-9_]*(KEY|TOKEN|SECRET|PASSWORD|PASS|PWD|URL|URI|AUTH|CREDENTIAL)[A-Z0-9_]*=|export [A-Z0-9_]+=)' ~/.zsh_history |
抓 export FOO_TOKEN=...、API_KEY=... 这类一行式赋值。
6. 排查 curl / 请求头泄漏
1 | grep -nEi '(curl|wget|httpie|http) .*(Authorization|Bearer|X-API-Key|api-key|token)' ~/.zsh_history |
7. 排查云服务 / 部署工具的登录与密钥命令
1 | grep -nEi '\b(aws|az|gcloud|vercel|netlify|supabase|firebase|docker|npm|pnpm|gh)\b.*\b(login|auth|token|secret|credentials|env)\b' ~/.zsh_history |
8. 排查订阅 / 科学上网链接
1 | grep -nEi '(subscription|shadowrocket|clash|mihomo|vless://|vmess://|trojan://|ss://|trojan|proxy)' ~/.zsh_history |
9. 排查 URL 中的 UUID(常见于订阅链接)
1 | grep -nEi 'https?://[^[:space:]]*[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}[^[:space:]]*' ~/.zsh_history |
四、命中了之后怎么办?
主动审查后,可以选择删除本地的历史记录,如果担心凭证存在泄漏风险。 可以按照优先级建议排查:
- 真实的 token / API key(OpenAI、Anthropic、GitHub PAT、NPM Token……)—— 立刻去对应平台 revoke 并重新签发。
- 订阅 URL / VPN 节点链接 —— 重置 UUID 或 token,旧链接会一直被滥用流量。
- 数据库连接串、Redis / MongoDB URI —— 改密码,必要时调整网络访问白名单。
- 带密码的 HTTP(S) URL —— 改密码,并检查 git remote、~/.netrc 等地方是否同步残留。
- 云服务凭证(AWS Access Key、GCP Service Account JSON 内容)—— 走云厂商的 rotate 流程,并检查 CloudTrail / Audit Log 是否有异常调用。
五、把好习惯固化下来
- 行首加空格这件事,要练成肌肉记忆:复制任何含密钥的命令,先按一下
Space再粘贴。 - 能用文件读取的就不要写在命令行:
curl --header @auth.txt、gh auth login --with-token < token.txt、docker login --password-stdin都是更好的形态。 - 真敏感的密钥,长期方案是放进 **macOS Keychain / Windows Credential Manager /
pass/ 1Password CLI /gh secret**,让命令行里只出现引用名,不出现明文。 - 给自己设一个每月一次的「翻历史」提醒——供应链事件大多在事发后才被披露,定期回头审计比一次性清理更重要。
终端历史不是你的备忘录,是攻击者的字典。少留一点东西在里面,下次出事的时候,你就少一份要连夜轮换的清单。