litellm遭投毒?Python供应链安全自查与止损指南
2026/9/13 19:49:52 网站建设 项目流程

这两天 litellm 被投毒的消息在开发群里和热搜上同时炸开了锅。如果你也是跑大模型应用的开发者,手头大概率装了 litellm——这个开源的统一 API 网关,平时用它接 OpenAI、Anthropic、本地 vLLM,省事是真的省事。可一旦这种基础设施级的库被混入恶意代码,后果就不是删个包那么简单了。这不是危言耸听,而是真实发生在开源组件供应链上的安全事件。攻击者不需要攻破你的服务器,只需要让你装到一个“看起来一模一样”的坏包,就能把你环境变量里的 API Key、内部配置、甚至是模型服务地址全部打包带走。

这篇文章不聊虚的,只讲三件事:第一,litellm 为什么会被盯上,所谓的“投毒”通常走哪几条路;第二,用一套可复现的自查流程,教你 20 分钟内判断自己的机器到底中招没有;第三,如果已经踩雷,怎么止损、怎么溯源、怎么避免下次再中招。适合所有用 Python 装过 litellm、或者在自己的项目里间接依赖了 litellm 的开发者。看完之后,你不需要成为安全专家,也能给自己做一次初步体检。

1. 风波源头:litellm 为什么会被投毒盯上

1.1 litellm 在开发者栈里的位置

先简单对齐一下背景。litellm 是一个 Python 编写的开源库/服务,核心能力是把各家大模型 API 统一成一套 OpenAI 风格的接口。你只需要改一行 base_url,就能从 GPT 切到 Claude,再切到本地部署的 Qwen,中间的重试、限流、负载均衡、成本统计它都帮你做掉了。很多 AI 应用、企业内部网关、自动化脚本里都跑着它,而且为了方便,服务进程常驻后台,权限通常还不低。

这种“承上启下”的位置决定了它的价值:对上层应用来说,它是流量入口;对下层模型服务来说,它是唯一出口。攻击者只要污染了这么一层,就能在你毫无感知的情况下拿到所有经过它的请求内容,包括你最值钱的东西——模型厂商的 API Key。更麻烦的是,litellm 本身是一个开源项目,依赖链很长,同名的仿冒包、被篡改的历史版本、伪装成正确依赖的小工具,随便哪一环出问题,都可能把恶意代码带进来。

1.2 攻击者要的是什么,投毒代码一般干什么

很多人以为“投毒”就是黑客闲着没事搞破坏,实际完全不是。投毒的目的都很直接,总结起来无非四类:

  • 偷凭据:扫描进程环境变量、读取.env文件、读取~/.aws/credentials、抓取/proc/self/environ,然后通过网络把数据发到远程服务器。这是最典型的,因为大模型 API Key 直接挂钩账单,拿到就能白嫖甚至转卖。
  • 植入后门:在包安装时往系统里写一个守护进程,或者注册一个定时任务,方便后续远程命令执行。常见手法是改crontab、写systemd服务、往 shell 启动文件里塞一键拉取的命令。
  • 挖矿:悄悄占用 GPU/CPU 跑挖矿程序。这类代码不爱联网外传数据,反而隐蔽性更强,机器变卡、电费变高才发现。
  • 篡改配置:改掉 litellm 的路由配置,把本该发到 OpenAI 的请求转发到攻击者自己的服务器上,实现中间人窃听。这个最阴险,因为业务功能完全正常,就是结果偶尔有点“怪”。

1.3 所谓“投毒”通常走的三条路

结合目前网上的讨论范围和供应链投毒的历史案例,litellm 相关的投毒事件大概率逃不出下面三种路径:

  • 名字仿冒与依赖混淆:攻击者注册一个和 litellm 极度相似的包名,比如lite-llmlitellmmlitellm-client,或者利用某些内部源的依赖混淆漏洞,诱导开发者安装到恶意包。只要你在 pip install 时打错一个字母,或者内部源里恰好存在同名恶意包,就会中招。
  • 官方版本被污染:某个版本的 wheel 或 sdist 在发布后被替换成带毒版本。这种事件比较罕见,但一旦发生影响面巨大,因为所有从官方源拉取该版本的人都会被波及。验证方法就是做哈希比对,后面会讲。
  • 依赖链上某个上游包被投毒:litellm 本身没问题,但它依赖的某个小库被污染,导入时被连带加载执行。这种最隐蔽,因为你检查 litellm 目录完全干净,问题却在更深的地方,靠肉眼很难发现。

不管事件源头最终指向哪种路径,对我们普通开发者来说,自查的逻辑是通用的:先确认安装来源,再查代码异常,最后看运行行为。

2. 自查第一关:确认你装的 litellm 来源和版本到底对不对

2.1 先看看本地装了哪个版本、从哪来

第一步永远不是去翻源码,而是先弄清楚你机器上到底装了什么。打开终端,执行:

pip show litellm

重点关注几个字段:

  • Version:看版本号是不是你预期的那一个,如果出现一个你从没见过的版本,要警惕。
  • Location:看安装目录是不是标准路径,如果安装在/tmp、项目目录、或者奇怪的 venv 里,要问自己为什么。
  • Installer:看是 pip、uv 还是 poetry 装的。
  • Requires:看依赖列表里有没有奇怪的新增项。

如果你项目用的是 uv,对应命令是:

uv pip show litellm

如果你跑在 Docker 里,需要进入容器执行同样的检查:

docker ps | grep <你的容器名> docker exec -it <容器名> pip show litellm

另外,litellm 也可能是作为间接依赖被装进来的,你自己没主动安装过它。这种情况更要查,因为越是藏在依赖深处的包,越不容易被注意到。可以快速确认:

pip freeze | grep -i litellm

2.2 和 PyPI 官方版本做哈希比对

看到版本号之后,去 PyPI 页面或者 GitHub Releases 看一下这个版本是否真实存在、是否已经被 yanked(撤销)。如果本地版本号在官方已经完全搜不到,说明来源可疑程度很高。

接下来做哈希比对,这是判断包是否被篡改最硬核的手段。先用官方源把同版本包下载到本地:

pip download litellm==<你本地的版本号> --no-deps -d ./verify

然后分别计算下载包和本地安装文件的 SHA256:

sha256sum ./verify/*.whl sha256sum ./verify/*.tar.gz # 再去 site-packages 里对比 sha256sum <pip show 的 Location>/litellm/version.py

如果你本地文件的哈希和官方源下载的哈希对不上,那基本可以确认你装的东西不是官方原包。即使对不上也不必慌张,可能是内部源对包做了二次打包,但后续需要重点排查。

2.3 检查 pip 和 uv 的源配置

很多投毒案例不是官方仓库被黑,而是你的包管理器源被改了。执行:

pip config list

看有没有index-urlextra-index-url指向非官方地址。同样检查环境变量:

env | grep -i -E "pip|uv|pypi"

常见的安全隐患是:公司内部源本意是为了加速,但缺少校验和审计,内部源上可能被放置了恶意同名包。还有一类问题是开发者的全局 pip 配置残留了一些公共镜像源或者快餐式代理源,这些源本身不坏,但没有官方源那么严格的发布审核。

对于有requirements.txtpyproject.toml的项目,还要检查依赖声明里有没有出现--extra-index-url--trusted-host这类参数。一旦发现,要确认是项目有意为之还是被改动过。这一步能帮你排除掉大部分“手滑装错源”的情况,毕竟官方仓库被真正攻破的事件极少,反而是各种自定义源和混淆包名更常见。

3. 自查第二关:从代码层面找出隐藏恶意逻辑

3.1 定位 litellm 的真实目录

表面检查做完,接下来要动真格翻代码。先拿到 litellm 的绝对路径:

python -c "import litellm; print(litellm.__file__)"

正常情况下会输出类似/usr/local/lib/python3.11/site-packages/litellm/__init__.py的路径。如果你发现它导入的目录是用户目录、临时目录,甚至是一个看起来像临时生成的哈希目录,那问题就很严重了。

3.2 搜索恶意代码特征码

进入 site-packages 下的 litellm 目录,搜可疑特征。手工用 grep 可以快速了解情况,但更推荐写一个小脚本扫全目录,因为子文件多的时候肉眼看不完:

import pathlib import re SITE = pathlib.Path("/usr/local/lib/python3.11/site-packages/litellm") patterns = [ r"exec\(", r"eval\(", r"base64\.b64decode", r"os\.system", r"subprocess\.(Popen|call|run)", r"requests\.(get|post)", r"urlopen", r"getenv", r"socket\.", r"crontab", r"nohup", ] for p in SITE.rglob("*.py"): try: text = p.read_text(encoding="utf-8", errors="ignore") except Exception: continue for line_no, line in enumerate(text.splitlines(), 1): if re.search("|".join(patterns), line): print(f"{p.relative_to(SITE)}:{line_no}: {line.strip()[:120]}")

跑完之后你大概率会看到一堆结果,先别慌。litellm 本身是网络库,有requests.post太正常了。关键要区分两类情况:

  • 正常调用:在函数内部、参数来自配置、URL 是官方 API 地址、行为在文档里有说明。
  • 异常调用:在模块顶部、import 时立即执行、URL 是硬编码 IP 或陌生域名、读取环境变量后马上拼接进去、结果写入/tmp或者~/.cache的隐藏文件。

3.3 重点检查 import 时会执行什么

投毒代码最喜欢藏在__init__.py里,因为只要import litellm就会加载。用 Python 的详细模式看导入过程:

python -v -c "import litellm" 2>&1 | tail -100

你会看到 Python 逐个加载了哪些模块、从哪个路径加载的。如果发现有人在导入阶段加载了非 litellm 相关的模块,比如_requests_forwardermetrics_exportertelemetry_sink这类看起来像是内部回调但实际陌生名字的模块,就要立刻去 site-packages 里找这个文件,看它做了什么。

还有一种常见情况:litellm 目录下有额外的.so.pyd文件。正常情况下纯 Python 包不应该有编译产物(除非用了 C 扩展),这些文件也常见于恶意代码,可以检查文件的创建时间和哈希,必要时上传到 VirusTotal 看检测结果。

3.4 检查最近被创建或修改的文件

投毒代码往往是后来被塞进已经安装的包里的,所以文件时间戳是个很好的线索。查看 site-packages 里最近一周内被修改的 Python 文件:

find /usr/local/lib/python3.11/site-packages -name "*.py" -mtime -7 -newer /tmp find /usr/local/lib/python3.11/site-packages -name "*.so" -mtime -30

重点排查两类位置:一是 site-packages 顶层多出来的新目录,二是用户的~/.local/lib目录。有些投毒包会专门把恶意模块写到用户级目录,让你在系统级目录里找不到任何线索。

4. 自查第三关:运行时行为和网络外联排查

4.1 进程层面有没有异常

代码层面查完,再查实时行为。先看进程:

ps aux | grep -i litellm

正常的 litellm 进程应该由你的启动命令拉起,可能是python -m litellmlitellm --config xxx,或者在一个明确的 Python 解释器下面。如果你看到一个用户是 root、父进程是 init、命令行特别乱的 litellm 进程,那很可能是被外部写进去的守护进程。

同时看一眼 CPU 和内存占用:

top -o %CPU -n 1

如果常规业务没跑,但 CPU 一直被打满,尤其是某个名字很正常的 Python 子进程占用了 300% 以上的 CPU,就要高度怀疑挖矿木马了。

4.2 网络外联:有没有发往陌生地址的连接

投毒代码偷到数据总得发出去,所以网络连接是重灾区。用 lsof 或 netstat 查看 Python 进程的连接:

lsof -i -P -n | grep python # 或者 netstat -anp 2>/dev/null | grep python

重点看两类:

  • 持续对外连接:一个 Python 进程保持着到陌生 IP 的长连接,数据量在持续增加。
  • 频繁发往非常规端口:比如 80、443、8080 之外的端口,尤其是 4444、5555、6667 这类常见于 C2 和后门的端口。

如果你发现 litellm 进程连了一个你完全不认识的外部 IP,可以把 IP 拿去查一下归属,但不能只靠归属判断,有的攻击者会把数据发到云厂商对象存储或者消息队列上,域名看起来还挺正经。关键在于:这个地址是否在你正常使用的服务列表里,如果不是,就要深究。

4.3 定时任务和开机启动项

投毒代码为了持久化,一定会想办法让它在机器重启后还能跑起来。检查这几种常见持久化位置:

crontab -l ls -la /etc/cron.d/ /etc/cron.hourly/ /etc/cron.daily/ systemctl list-timers systemctl list-unit-files | grep -E "litellm|python"

同时检查 shell 启动文件:

grep -n -E "litellm|curl|wget|python" ~/.bashrc ~/.zshrc ~/.profile ~/.bash_profile 2>/dev/null

macOS 用户还要额外看 LaunchAgent:

ls -la ~/Library/LaunchAgents/ launchctl list | grep -i litellm

看到 cron 里有一行python -c "import requests; ..."、或者 systemd service 的 ExecStart 指向了一个奇怪脚本,这些基本就是持久化后门。不要犹豫,把那行内容保留截图之后再做清理。

4.4 日志与服务商审计记录

最后一步可能很多人会忽略,那就是去模型服务商的控制台看审计记录。litellm 的 Key 被偷走后,攻击者通常会立刻发起试探性调用,可能是问你当前模型列表、可能发几条极短的聊天请求,然后才正式开始薅羊毛。去 OpenAI、Anthropic、Azure OpenAI 等控制台里拉取最近几天的用量记录,重点看:

  • 是否出现陌生 API Key 的调用
  • 是否在凌晨凌晨时段有不明请求
  • 请求的 model 是不是你项目里从没用过的
  • 调用 IP 来源是否分布在一个奇怪的地区

如果发现了,那就不仅是“可能中招”,而是“已经确认失窃”,要立刻进入下一节的止损流程。

5. 确认中招后的止损操作:隔离、换密钥、清后门

5.1 第一时间隔离,不是先删包

如果前面任何一步确认了异常,第一件事不是急着pip uninstall,而是先把机器隔离。断网、从内网摘掉、停止对外服务。原因是:保留现场才能做更完整的取证,比如内存里的进程信息、建立的网络连接、写入磁盘之前的数据,这些都是后续判断攻击范围的重要依据。

具体操作上,如果你跑在云服务器上,建议在控制台做安全组变更,只保留你的管理 IP 可以 SSH,而不是直接关机。如果你跑在本机,直接把网线/无线网络断开。隔离的目的是切断攻击者继续访问你机器的通道,同时保住你后续查日志的可能。

5.2 轮换所有关联凭据

这一步的重要性超过删恶意文件本身。攻击者偷到的 Key 在你清理完木马之后依然有效,所以必须先让它们全部失效,再发新的。

需要轮换的凭据清单包括但不限于:

  • 所有模型服务商的 API Key(OpenAI、Anthropic、Azure、谷歌、阿里、百度、智谱等)
  • 数据库连接串和密码
  • 云平台 AK/SK
  • Redis、消息队列等中间件的密码
  • 任何写入过环境变量、config 文件、请求头里的内部 Token

不要抱着“我这个 Key 只用在测试环境,没关系的”这种侥幸心理。投毒代码一旦偷走 Key,会用很短的时间完成批量试探,测试环境的 Key 也可能被拿去盗刷。

轮换时要特别注意,先撤销旧的再创建新的,避免新老 Key 在一段时间内同时在线上。同时确认你的应用配置已经更新,不要让服务在重启后带着旧 Key 继续跑。

5.3 清理恶意文件与持久化后门

轮换完密钥,再回头清后门。根据前面的检查结果,把可疑文件逐一处理,但建议先记录下文件路径和 SHA256 再删除,方便后面喂给安全工具分析。

清理顺序建议:

  1. 删除 site-packages 下确认的恶意文件和包。
  2. 删除 crontab、systemd、LaunchAgent 里的恶意条目。
  3. 删除 shell 启动文件里的恶意行。
  4. 检查/tmp/var/tmp~/Downloads下有没有新出现的可执行脚本。
  5. 检查 Python 包管理器配置里有没有被篡改的源地址,恢复成官方源。

如果你用的是 Docker,最干净的做法是直接重建容器镜像,而不是在容器里删文件,因为容器文件系统里可能还有隐藏的修改点。重建之后,用新的镜像重新部署服务,并确认没有多余的持久化挂载。

5.4 溯源和复盘

止损做完,还要问一句:我是怎么中的招?是装了一个仿冒包?还是内部源被污染了?还是某个同事分享的脚本里带了恶意包?这一步决定了你的防护措施有没有针对性。

翻一下 shell 历史:

history | grep -i -E "pip|uv|install"

查一下最近的 pip 安装时间点:

ls -l --time-style=full-iso /usr/local/lib/python3.11/site-packages/litellm

对比恶意文件的创建时间和你的操作记录,通常能还原出“哪天、我执行了什么命令、中招了”。如果实在找不到直接原因,就把这次的恶意文件 hash 保存好,提交到安全社区做情报共享,说不定能帮到其他人。

6. 给以后的自己加几道锁:供应链安全实践

6.1 锁定版本和哈希,从根上减少随机性

经此一役,最值得养成的习惯就是不再使用不锁版本的依赖安装方式。在requirements.txt里用精确版本号,不要用>=

litellm==1.40.0

配合 pip 的哈希校验模式:

pip install --require-hashes -r requirements.txt

--require-hashes会让 pip 只安装 hash 与记录一致的包,任何一个包被篡改都会导致安装失败。虽然维护成本高一点,但换来的确定性非常值。

如果项目用 uv,直接生成uv.lock,锁定整个依赖树;用 poetry 则生成poetry.lock。锁文件的意义在于:每个人在任何时间安装,拿到的都是同一份依赖组合,不会因为某天某个小版本被悄悄替换就中招。

6.2 更新依赖前先看 release notes 和 diff

很多投毒事件发生在你“无脑升级”的那一刻。不要一行pip install --upgrade litellm就跑,至少要:

  • 先在官方 GitHub 看 release notes,确认版本变更内容正常
  • 如果只升级了一个小版本,用git diff看代码差异,排除可疑提交
  • 升级后做一次最小冒烟测试,确认核心调用正常

这不是工作量大到不可接受的事,相反,对于基础设施级依赖,多花十分钟看 diff 能帮你躲掉 90% 的坑。

6.3 用代理仓库和扫描工具做缓冲

有条件的话,搭一个内网 PyPI 代理,所有开发者默认只能从内部源拉包,外部包必须经过审核才能同步进来。这样即使上游某个包被投毒,也不会瞬间扩散到所有人机器上,给了你一个缓冲窗口。

工具方面,在 CI 里加依赖审计扫描:

pip install pip-audit pip-audit

或者用osv-scannersafety,在每次构建时扫描依赖树里的已知漏洞。容器环境里做镜像扫描,同时生成 SBOM(软件物料清单),这样万一出现风险,你能精确知道哪些镜像、哪些容器受影响。

6.4 权限最小化和行为监控

最后是运行层面。litellm 这种纯转发服务完全不需要 root 权限,创建一个专用的低权限用户跑去:

useradd --system --no-create-home litellm-user

敏感环境变量只注入到需要它的容器,不要一股脑写进/etc/environment给所有进程共享。Linux 上还可以用 bpf 工具或者云安全组做基本的行为审计,重点监控“进程读取环境变量后立即发起外联请求”这类组合动作。不用一次上很重的方案,先从最小改动开始,逐步补。

写在最后的个人体会

我把我手上几台机器按上面的顺序过了一遍,最终确认没有中招,但过程中确实看到了不少平时根本不会注意到的系统细节,比如一堆早该清理的旧包、几个隐藏得很深的启动脚本、还有一些权限宽到没边的环境变量。说实话,如果不是这次“瓜”上了热搜,我大概率还会继续带着这些隐患跑很久。所以我的建议是:别等事情爆出来才想起来做检查,现在照着上面的五步走一遍,二十分钟就能让心里有底。以后再装任何 Python 包,都当成“可能会出事的包”来看待,装之前看一眼源、锁一下版本、装完扫一遍,这个习惯能帮你避免非常多麻烦。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询