1. 事件复盘:litellm 投毒到底是怎么发生的
1.1 先搞明白 litellm 在我们的架构里是什么角色
litellm 说白了就是大模型时代的“流量网关”。不管你是接 OpenAI、Anthropic、Azure 还是本地部署的 Ollama,只要走 litellm proxy,上层业务只认这一个地址,底层模型随便换。这也是为什么 litellm 在近两年火得快,很多团队直接把它当公司内部的模型代理底座来用。
问题恰恰出在这个位置。普通的开源库被投毒,顶多是你某个功能不好使,或者数据被偷走一部分。但 litellm proxy 这一层被投毒,相当于你公司所有模型请求的“总水管”被人接了一根暗管。请求要经过它、API Key 存在它那里、日志从它这边出去,甚至连模型路由规则都由它决定。这层要是失守,就不只是丢一两个 Key 的事,而是模型调用、业务数据、内部逻辑全部暴露在攻击者眼皮底下。
这次事件在群里传开的时候,大家用的关键词就是“litellm 投毒”和“账号投毒事件(mini shai-hulud 蠕虫)排查通知”。我先解释一下大概发生了什么:攻击者没有去硬刚 litellm 官方仓库,而是通过各种方式诱导你安装被篡改过的依赖包、仿冒的同名包,或者在拉取镜像时拿到带后门的版本。一旦装上,恶意代码就开始扫描环境变量、读取代理配置文件、尝试把密钥和日志转发到外部服务器。
1.2 投毒的几个主要入口,别以为只有 pip install 才危险
很多人一听到“投毒”,第一反应是“我又没乱装包,不怕”。实际上这次 litellm 投毒的传播路径比想象中要宽,我把目前已知的几类入口梳理了一下:
| 投毒入口 | 典型场景 | 危险程度 |
|---|---|---|
| PyPI 仿冒包 | 用 litellm、liteLLM、llitellm 等相似名字发布恶意包,等人拼错装错 | 高 |
| 传递依赖被劫持 | litellm 正常,但它的某个子依赖上游被替换成恶意版本 | 很高 |
| Docker 镜像投毒 | 拉取 litellm proxy 镜像时用了非官方源或被篡改的 tag | 很高 |
| 内部源混入问题包 | pip 配置了私有源,私有源同步时没有做完整性校验 | 高 |
| 配置下发劫持 | 攻击者通过配置中心或环境变量注入恶意 model tag | 中等 |
这里特别要提醒的是“传递依赖被劫持”这条链路。litellm 本身的依赖树不算小,有 openai、anthropic、requests、pydantic 这些常见的包,任何一个上游出错都会顺着依赖关系进入你的环境。平时大家安装 litellm 时会看到一堆依赖一起装进来,没人会逐个检查这些包最近有没有发过带问题的版本。攻击者只要在某个依赖的某个版本里塞一段采集环境变量的代码,就能顺藤摸瓜拿到你在 .env 里写的那些 Key。
1.3 “标签投毒”这步操作,是这次事件里最阴的地方
这次排查通知里反复出现一个词叫“标签投毒”。我第一次看到的时候也没反应过来,后来仔细一看,才发现这个攻击思路确实刁钻。
litellm proxy 支持给模型设置别名、标签、负载均衡策略,比如“team-a-model”指向 gpt-4o,“team-b-model”指向 claude-sonnet。攻击者投毒之后,不直接改你的代码,而是去改模型配置数据库或部署文件里的标签映射关系。这样做的效果是:你以为自己在调公司内部最便宜的那个模型做测试,实际流量已经被悄悄导到攻击者控制的服务器上。你发的 prompt、传进去的业务数据,全经过别人的手。
这种攻击最难防的一点是,它不会让系统报错,也不会让接口突然变慢。你只会在月底对账的时候发现某个模型的调用量对不上,或者某些“明明没有调用记录”的数据泄露出去。但因为走的是同一个 litellm 入口,日志可能还被攻击者抹掉了一部分,查起来特别费劲。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 自查清单:怎么快速判断机器中招了没有
2.1 检查安装来源和依赖树,这一步别跳
收到排查通知之后,第一件事不是去看流量监控,而是先把你机器上装的那个 litellm 是从哪来的、装的是什么版本。先跑一遍这几条命令看看现状:
bash复制# 查看 litellm 当前版本和安装路径
pip show litellm
# 列出完整依赖树,确认是否有可疑的包
pip show litellm | grep -E "Version|Location"
pipdeptree -p litellm
如果 pipdeptree 没装,可以用 pip freeze 先看个大概:
bash复制pip freeze | grep -i "litellm\|openai\|anthropic"
另外建议直接查一下 litellm 包的安装路径下的文件,看看有没有明显不对劲的 py 文件。正常 litellm 的代码目录结构是有规律的,大部分功能模块都在 litellm/ 目录下。如果发现多了一些看不懂的文件夹,或者某个 py 文件被改过大小的,要当心。
bash复制# 查看 litellm 包路径
python -c "import litellm, os; print(os.path.dirname(litellm.__file__))"
# 找一找最近 7 天内新增或者被修改过的文件
find /你的/python/site-packages/litellm -name "*.py" -mtime -7
我这边还验证了一下版本号是否跟官方发布一致,直接请求 PyPI 的接口拿最新版本号做对比:
bash复制curl -s https://pypi.org/pypi/litellm/json | python3 -c "import sys, json; data=json.load(sys.stdin); print(data['info']['version'])"
如果两者对不上,那就要格外留意了。当然版本对不上不代表一定被投毒,也可能只是内部源同步慢,但至少说明你的安装来源可能不是官方最新。
2.2 搜异常外联和网络行为,这是中招与否的关键证据
很多投毒代码在安装之后会立刻发起外联,把收集到的数据传出去。因为它的目标是尽快拿到密钥,所以外联行为往往发生在安装后的几分钟内。如果你在安装过 litellm 之后没有专门盯过网络流量,现在可以靠现有日志补查。
Linux 服务器上先看有没有可疑的连接:
bash复制# 查看当前所有对外的 TCP 连接
netstat -antp | grep ESTABLISHED
# 或者用 lsof 查看特定进程的网络连接
lsof -i -P -n | grep python
如果你用的是 Docker 部署的 litellm proxy,先把容器里所有进程翻一遍:
bash复制docker ps -a
docker top <容器名>
# 进入容器看是否有额外启动的 python 进程
docker exec -it <容器名> ps aux
异常外联有两种典型的特征:一是连接的目标域名很奇怪,跟 litellm 官方域名没有任何关系;二是连接频率没有规律,有时候隔几分钟蹦一次,有时候凌晨突然活跃。如果在服务器日志里看到某个 IP 定期往国外服务器发数据包,而且包的体积不大、但很频繁,基本可以判定是有东西在往外传数据。
我平时还习惯加一个“域名白名单”的思路,litellm proxy 自身只需要访问你在配置里写的大模型服务商域名。如果发现它除了访问 api.openai.com、api.anthropic.com 这些域名之外,还频繁请求一个完全无关的地址,那就不是正常行为。
2.3 检查 .env、环境变量和代理配置是否被动过手脚
litellm proxy 最常用的配置方式就是 .env 文件,里面通常躺着 OPENAI_API_KEY、ANTHROPIC_API_KEY、DATABASE_URL 这类敏感信息。投毒代码最优先盯的就是这些。
所以自查的时候一定要确认几件事:.env 文件什么时候被改过、里面有没有被追加奇怪的键、环境变量里有没有多出攻击者自定义的项。
bash复制# 查看 .env 文件最近修改时间
stat .env
# 查看当前 shell 环境中与 key 相关的变量
env | grep -i "API_KEY\|LITELLM"
再打开 litellm 的配置文件看一遍,重点看 model_list 里面有没有不认识的模型名字,尤其是那些指向非官方域名或陌生 IP 的模型配置。如果你发现某个模型的名字特别长、或者里面夹杂着十六进制字符串,有可能是攻击者注入的标签投毒入口。
2.4 查 cron、systemd 服务和启动自启项
投毒代码为了持久化,通常会做“二次启动”的操作,确保你把主进程杀掉之后它还能再活过来。常见的手法包括写 crontab、创建 systemd 服务、往 .bashrc / .zshrc 里追加启动项。
bash复制# 查看当前用户的 crontab
crontab -l
# 查看系统所有 cron 任务
cat /etc/crontab
ls -la /etc/cron.d/
# 查看 systemd 服务里有没有新增的可疑服务
systemctl list-units --type=service --state=running | grep -v "systemd\|ssh\|docker"
我自己遇到过一种情况,攻击者把恶意 python 脚本写在 /tmp 目录下,然后通过 crontab 每隔 5 分钟执行一次。因为不是通过任何标准方式安装的,常规的杀毒软件根本不会盯 /tmp 下面的文件。排查的时候顺手看一眼 /tmp 里有没有可疑的 .py 文件,也是一个重要动作。
3. 中招后系统会有什么异常,mini shai-hulud 蠕虫的行为逻辑拆解
3.1 恶意代码在机器上做的事情是什么
这次排查通知里提到的 mini shai-hulud 蠕虫,我把它理解成一种以“读取模型密钥、劫持代理路由、长期驻留”为目标的恶意样本。它的攻击链条并不复杂,但每一步都踩在 litellm 的痛点上面。
首先是密钥收集。它会遍历当前进程的环境变量,以及常见路径下的 .env 文件,把所有长得像 API Key 的字符串全部抓出来。抓取范围包括但不限于以 OPENAI、ANTHROPIC、AZURE、GOOGLE、HUGGINGFACE 开头的变量。
然后是对外传输。拿到密钥之后找一个隐蔽的出口域名,把密钥内容编码后发送出去。攻击者通常会同时做一层“域名前置”或者用 CDN 域名做中转,让流量看起来不像恶意请求。
再往后就是行为劫持。如果运行的是 litellm proxy,恶意代码会尝试修改模型映射表,把某些标签下的模型请求转发到攻击者的服务器。这个阶段最隐蔽,因为你的服务还在正常返回结果,只不过返回结果的人已经换了。
最后则是持久化。通过写 crontab、安装定时任务等方式保证重启后依然能运行。我印象比较深的一个细节是,有些样本会把自身复制成跟系统服务很像的名字,比如 “system-update.py”,放在不那么起眼的目录里。
3.2 为什么“账号投毒”比单纯丢 Key 更让人头疼
投毒事件里很膈应人的一个点是“账号投毒”。攻击者拿到你的 API Key 之后,不只是偷偷调用,还会把 Key 的信息改掉,或者绑定到自己的账号体系里去。等你发现账单异常,打算把 Key 删掉重发的时候,发现某个项目下的 Key 已经被标记成了别的用途,甚至被替换成了攻击者的专属 Key。
这就好比有人拿了你的门禁卡,不但进门偷东西,还把你卡上的权限信息改了,让你自己拿着卡反而刷不开门。处理这种问题远比“删掉一个 Key”麻烦,得去控制台手动核对每一个 Key 的归属、权限和使用记录。
如果你用的是 litellm proxy,同时接入了多个模型服务商,那要把每一个服务商控制台里的 Key 全部轮换一遍。我当时的做法是先把 litellm 停掉,然后逐一到各个服务商后台删掉旧 Key、创建新 Key,再重新填入 litellm 的配置里。绝对不能在旧 Key 还有生效状态的情况下重启 proxy,否则等于把自己的新钥匙又递到攻击者手里。
3.3 现场排查时优先保留哪些证据
很多团队在发现中招之后的第一反应是赶紧杀进程、删文件,这个操作我不太推荐。投毒事件的排查需要保留第一现场的痕迹,否则后续想追溯攻击源头、判断数据泄露范围会很难。
建议优先保留这几类证据:
- 恶意进程的启动命令和 PID
- /proc 目录下对应进程打开的 fd 信息
- 近 7 天内修改过的所有文件列表
- 相关时间段内的访问日志和代理日志
- 异常外联的 IP 和域名
- 当前机器上的 crontab 和 systemd 配置快照
这些信息保留下来之后,哪怕暂时没法完全还原攻击链,至少能交给安全团队做进一步的样本分析。我一般是先把这些证据打包放到一个隔离目录,然后立刻断开机器的外网连接,再开始清理操作。
4. 应急清理与恢复:一步一步把机器拉回干净状态
4.1 第一步先断网隔离,别急着删东西
发现中招之后动手要快,但别乱。第一步是断网或者拉出安全策略,把受影响的机器隔离起来。如果你跑的是云服务器,可以先把安全组规则改掉,拒绝所有出站流量;如果是本地开发机,直接把网线或 Wi-Fi 断开,或者关掉进程的外网访问权限。
这一步的目的是阻止恶意代码继续外传数据。只要网络通路还在,你删除进程、改配置文件的动作都可能被攻击者看到,甚至会被二次上传新的样本。所以不要嫌麻烦,先物理隔离再谈其他。
隔离完成之后,把自己能想到的账号、密钥提前做一遍轮换。现在的轮换速度取决于你的服务商支持多快生效,理论上应该在断网后立刻进行,而不是等到排查完成之后再操作。
4.2 清理恶意文件、进程和自启项
处理恶意文件时,我习惯按“进程 - 文件 - 自启动项”三个层面来清理。先在系统进程列表里找出 python 相关的可疑进程,记下 PID 和启动命令后直接 kill。然后去查看进程打开的脚本路径,把对应的恶意文件剪切到隔离目录,而不是直接删除,方便后续分析。
接着清理自启动项。查看 crontab、systemd、/etc/init.d/ 目录,把涉及恶意脚本的条目全部移除。如果你用的是 mac 或 Windows 开发机,还要检查 launchd 和注册表启动项。这一步最容易漏掉,因为很多恶意代码不只会写一个地方,它会同时埋好几个自启动入口,只清理一处,重启之后照样会重新运行。
清理完成之后重启机器,再检查一遍 crontab 和进程列表,确认没有新增的可疑项目。
4.3 重建 litellm 环境并锁定依赖版本
清理掉恶意代码不等于环境就安全了。你还要把 litellm 以及它的整个依赖树重新装一遍,最好是装到全新的虚拟环境里,不要在原环境上修修补补。
bash复制# 创建全新虚拟环境
python3 -m venv venv-litellm
source venv-litellm/bin/activate
# 安装官方指定版本并锁定全部依赖
pip install --only-binary=:all: litellm==指定版本
pip freeze > requirements-lock.txt
如果项目里本来就有 requirements.txt,建议全部删掉重来,不要只改一个版本号。因为你没法确定当前环境中哪个包已经被污染,最稳妥的做法是把环境推倒重建,再按 lock 文件把所有依赖固定下来。
等环境重建完毕,打开 litellm 的 model_list 配置逐行核对,确认每个模型指向的都是可信的服务商域名,没有被篡改成奇怪的地址。再确认密钥是从密钥管理服务拉取的,而不是直接硬编码在代码里。
4.4 Docker 部署场景的恢复方案
如果你用的是 Docker 方式跑 litellm proxy,恢复流程不太一样。不能只把容器停了再重新跑一遍,因为本地挂载的卷、持久化数据里面可能还藏着恶意文件。
正确做法是把旧容器删掉,确认镜像 digest 是否来自官方,然后拉取明确可信的镜像版本重新创建容器。
bash复制# 查看当前镜像的 digest
docker inspect --format='{{index .RepoDigests 0}}' litellm-proxy
# 删除旧容器和旧镜像
docker rm -f <容器名>
docker rmi <镜像名>
# 拉取官方指定版本并启动
docker run -d --name litellm-proxy \
-p 4000:4000 \
-v /data/litellm:/app/data \
ghcr.io/berriai/litellm:具体版本
同时在启动命令里加上资源限制,比如只允许容器访问特定的域名,而不是直接放任网络访问。这能大大降低再次被投毒后数据外传的风险。
5. 排查实录与避坑技巧,这些比官方文档更值钱
5.1 中招现场最容易犯的几个错误
这次排查过程中,我见过不少团队踩了同一个坑:发现中招后,只关注 litellm 本身,忽略了它的核心依赖。比如你把 litellm 卸载重装,但依赖树里的 openai 或者 requests 仍然是被篡改过的版本,重新安装 litellm 时又把恶意依赖带回来了。正确的做法是直接推到虚拟环境重建,或者把所有依赖都重新安装一遍,确认来源合法。
另一个高频失误是没有在第一时间检查日志里的注入痕迹。litellm proxy 本身有比较完整的日志,里面会记录每一次请求的来源、模型和目标地址。如果发现某些请求的目标模型根本不在你的配置列表里,那基本可以确定配置被改过。对比配置文件和日志是快速定位受害范围最直接的方式。
5.2 官方源、版本锁定和缩小的攻击面
经过这次事件,我认为 litellm proxy 的最佳实践必须包含下面几个基础动作,缺一不可。
第一个动作是安装时不要用裸的 pip install,要固定到具体带哈希的依赖。可以用 pip-tools 或者 poetry 把整个依赖树生成哈希,安装时校验一遍。这样哪怕上游被篡改,哈希对不上也装不进去。
第二个动作是把 .env 文件从项目目录里移出去。litellm 允许通过环境变量传入数据库 URL 和各类 API Key,这些敏感配置建议从环境变量注入,而不是直接把 .env 放在代码目录里。如果必须用文件,记得把 .env 加入 .gitignore,并配置只有部署用户才有读取权限。
第三个动作是配置出口白名单。litellm proxy 在云服务器上跑的时候,可以借助云安全组限制只允许访问特定的大模型服务商域名,其他出站全部拦截。虽然不能防止恶意代码在进程内作恶,但至少能大幅降低数据外泄的概率。
第四个动作是对管理端加访问控制。litellm proxy 自带管理界面和 master key,不要用默认配置上线。生产环境里一定要开强密码、强密钥,并且通过防火墙限制管理接口只允许公司内网 IP 访问。
5.3 我整理的一张自查要点速查表
| 检查项 | 命令/位置 | 关注点 |
|---|---|---|
| 包版本 | pip show litellm | 版本是否对应官方最新 |
| 依赖树 | pipdeptree -p litellm | 是否有不认识的包混入 |
| 安装目录文件 | find site-packages -name "*.py" -mtime -7 | 近期新增/修改的脚本 |
| 外联连接 | netstat -antp | 是否有陌生 IP/域名 |
| 环境变量密钥 | env | grep -i api_key | Key 是否泄露 |
| 模型配置 | litellm config.yaml | 是否有陌生模型标签 |
| 自启动项 | crontab -l / systemctl | 是否有可疑定时任务 |
| 代理日志 | litellm 日志目录 | 请求是否到达陌生模型 |
| Docker 镜像 | docker inspect RepoDigests | 镜像 digest 是否可信 |
6. 常见问题排查实录:遇到这些情况别慌,按这个顺序处理
6.1 “我只是准备测试,还没上线,也需要处理吗”
需要。很多人觉得 litellm 只是本地研究用的工具,中招也无所谓。但投毒代码的目标不只是生产环境的密钥,它会把本地环境变量里的个人 Key、公司测试账号的 Key 全部传走。哪怕只是本地开发机,只要里面存在有效的 API Key,攻击者就能拿去调用模型,产生的费用由你的账号承担。
而且本地开发机往往连着公司的内网、代码仓库、跳板机,被投毒后可能进一步向内部横向渗透。所以不要觉得“没上线就安全”,发现异常就该按同样流程处理。
6.2 “密钥已经泄露了,只把机器清理干净够吗”
不够。机器清理只是第一步,密钥轮换必须同步做。如果只清理机器不换 Key,攻击者手里仍然拿着有效的 API Key,随时可以继续调用你的模型、读取你的私有数据。而且有些服务商的 API Key 是项目级别的,删除之后要检查一下同一个项目下有没有新增的陌生 Key。
换完 Key 之后,建议把这一次泄露相关的调用记录、账单信息保存下来,后续如果有账号被盗用的争议,这些记录可以作为判断依据。
6.3 “怎么判断投毒是不是从 Docker 镜像进来的”
如果你用的是 Docker 镜像,判断方法不难。先检查镜像的 digest 是否和官方发布的一致。如果镜像 tag 写的是 latest,那建议立刻去官方仓库核对最新 digest,如果发现本地镜像 digest 与官方最新不一致,说明拉到的镜像不是同一个。
另一个方法是看镜像构建记录,docker history 可以列出镜像每一层执行过的命令。正常 litellm 官方镜像的构建步骤是清晰可查的,如果看到某一步执行了一段奇怪的 python 代码,或者下载了一个不明来源的压缩包,那这个镜像大概率有问题。
6.4 “litellm 被投毒是不是就永远不能用了”
不是。litellm 本身是一个优秀的开源项目,这次的事件提醒我们的是不要在依赖管理上偷懒。做好版本锁定、依赖校验、密钥管理和网络隔离,litellm proxy 仍然可以稳定使用。
坦白讲,任何使用广泛的工具都可能成为攻击目标,litellm 会有被投毒的事件,其他工具将来也可能有。重要的是保持“最小权限”和“可控变更”的习惯,不要随手安装来源不明的包,也不要把密钥裸放在环境里不管。
我自己在排查完环境之后,最大的感触是很多麻烦本来是可以避免的。多花十分钟做依赖锁定和权限收敛,好过事后花几个小时去追溯泄露路径。这次 litellm 投毒事件也算是一次提醒,建议你趁这个周末把你机器上所有跟 litellm 相关的部署都过一遍,确认版本来源、依赖来源、密钥存放方式都没有问题。别等下次告警到群里,才发现自己连第一条确认命令都还没跑过。
