做 AI 应用开发的朋友,对 litellm 应该都不陌生。它就像一个万能插座,把 OpenAI、Anthropic、Cohere、Hugging Face 这些各家模型的接口统一成一个 OpenAI 兼容格式,团队内部只要维护一套调用逻辑就行,省心是真省心。但正因为它太重要,一旦这个库被投毒,影响面就不是单个服务的问题,而是所有依赖它的网关、后台任务、内部工具全部中招。
最近圈子里流传的 litellm 投毒风声,说实话没有让我特别意外。模型网关本身就是整个 AI 应用链路里权限最重的一层,它握着各家模型的 API key,能访问内网,还往往跑在开发机或者服务器上。这种位置,注定是攻击者最想拿下的目标。这篇文章我就从实际排查的角度,带你过一遍机器上可能残留的恶意痕迹,同时聊聊怎么把后续的投毒风险压到最低。没中招的可以当作一次安全检查,中招的也别慌,后面给了应急处理的顺序。
1. 这次“投毒”事件到底是怎么回事
1.1 litellm 为什么值得被攻击者盯上
先冷静下来想想,litellm 被盯上不是偶然。它是一个分发量非常大的开源项目,很多公司直接把它当内部模型网关的底座,开发机上装一次,测试环境装一次,生产环境再装一次。攻击者投毒这种项目,收益是滚雪球式的:只要有一个版本被污染,所有升级到这个版本的用户都会中招。
更关键的是,litellm 的运行环境带有明显的“枢纽”属性。它要转发请求到各家大模型服务商,意味着服务器上一定存有可用的密钥;它要处理 model list、配置解析、日志上报,意味着它有网络访问能力;很多团队还会把 litellm proxy 部署在内网,方便其他服务调用。这三样凑在一起,就是一个高价值的攻击目标:拿到 litellm 进程的权限,基本就等于拿到了这个团队的大模型调用入口。
还有一个因素容易被忽略:这类项目的更新频率极高。大模型领域几乎每周都有新模型发布,litellm 要跟上节奏,就必须频繁发版。高频率更新会带来两个问题,一是用户养成了“有新版本就升级”的习惯,二是每次发版涉及的依赖变更、配置改动非常多,安全审查很难面面俱到。攻击者看中的就是这种“忙中出错”的机会。
1.2 投毒包可能藏在哪些环节
先说最常见的入口:包名仿冒。PyPI 上经常出现名字和官方包只差一两个字母的恶意包,有人手滑输错一个字符,就会把恶意包装进环境。litellm 这种“一个单词拼到底”的包名特别容易被仿冒,比如说多了个 h、少了个 l、中间多个横杠,不仔细看根本发现不了。所以排查的第一步,永远是确认自己装的包是不是官方那个。
第二个入口是依赖链投毒。litellm 本身依赖几十个第三方库,如果其中某个小依赖维护不积极,攻击者完全可以先拿下这个依赖的维护者账号,再往里面塞恶意代码。等 litellm 发新版本、用户升级的时候,恶意代码就通过依赖关系自动进入用户环境。这种投毒最难发现,因为问题不在 litellm 的代码里,而在它的依赖树深处。
第三个入口是镜像和安装脚本。有人会从 Docker Hub 拉取所谓的“litellm 一键镜像”,或者按博客教程执行一段包含 curl 下载、bash 执行的“快速安装脚本”。这些渠道如果来源不正规,里面的内容是谁也控制不了的。我见过不少团队为了方便,把整个安装过程写成一个 shell 脚本放在内部共享盘里,久而久之根本没人逐行审查这个脚本到底做了什么。
明白这几个入口后,后面的排查就有方向了:先看直接安装的包,再看依赖树,最后看进程和网络层面的可疑行为。
2. 机器中招排查:跟着这份清单走一遍
2.1 第一步:核查包来源与版本
不管你现在慌不慌,先从最基本的开始:确认你机器上的 litellm 到底是哪个版本、从哪来、装在了哪。打开终端,把下面这几条命令跑一遍:
pip show litellm pip list 2>/dev/null | grep -i lite python -c "import litellm, os; print(os.path.dirname(litellm.__file__))"第一条命令会显示包名、版本号、安装位置。第二条看有没有同时装了多个类似名字的包。第三条直接打印出 litellm 模块的真实路径。
看到信息后,对照官方 PyPI 页面确认一下版本号。如果显示的是一个你从没见过的版本,比如比官方最新版还“新”的,那要立刻产生怀疑;如果版本号正常但安装时间很蹊跷,比如你确定最近没有安装过它,时间却显示在几天前,那也要接着查下去。
接下来看安装源。PyPI 包在安装时,pip 会记录下载源地址,用下面的命令查看:
pip config list pip config debug重点确认 index-url 不是某个陌生域名。正规公司内部可能会搭私有 PyPI 镜像,这没问题,但如果你看到的是个人博客、网盘或者某个看起来就不像官方源的域名,就要小心了。这里说句实在话,我从没见过投毒事故是内网源导致的,但见过不少测试环境因为当初图省事,直接把 index-url 指向了某个早已没人维护的第三方源,后来那个源被攻击者接管,所有从它上面安装的包都有风险。
看完源之后,还需要检查 site-packages 里 litellm 目录的文件情况:
ls -latr $(python -c "import site; print(site.getsitepackages()[0])")/litellm | head -30这个命令按时间倒序列出 litellm 目录里的文件。正常情况下,包内文件的修改时间应该集中在安装或升级那几天。如果发现某个文件的时间特别新,或者出现了一些你完全没听过的文件名,比如update.py、telemetry.py、_internal.py之类,那就得重点看看里面的内容了。
还可以把它和官方仓库的文件列表比对。litellm 源码在 GitHub 上都是公开的,你直接去看仓库里是否有这个文件、文件内容是否一致。这一步不需要多高深的技术,就是细心活。
2.2 第二步:检查运行中的进程和端口
包层面的检查做完了,接下来看运行中的东西。litellm proxy 默认监听 4000 端口,但投毒代码不一定会用默认端口,所以不能只看 4000。
ps aux | grep -iE "litellm|python|node" | grep -v grep ss -tlnp | grep -E "LISTEN|ESTAB" lsof -i :4000这三条命令分别查:有没有 litellm 相关进程在跑、当前系统上所有监听和建立的网络连接、4000 端口被谁占用。重点是看进程列表里有没有启动用户不对的 python 进程,比如 root 用户跑了一个/tmp/python -m litellm,或者连接里有大量与未知 IP 的 TCP 连接。
这里有个经验要分享:投毒代码很多时候不直接改名,而是把自己伪装成正常的 python 进程,靠 ps 很难一眼发现。所以第二步我不会只看进程名,而是更关注“监听端口”和“外连地址”。如果一台内网机器,明明没有对外服务,却频繁建立到陌生公网 IP 的连接,这就是非常明确的警告信号。
外连检查可以用这些命令:
ss -tnp | grep ESTAB netstat -tnp 2>/dev/null | grep ESTABLISHED cat /etc/hosts/etc/hosts也很重要,脏东西可能在里面加一条域名映射,把正常的 api.openai.com 指向恶意服务器。这个文件平时没什么人改,一旦多了看不懂的记录,绝不正常。
2.3 第三步:排查持久化痕迹
投毒代码如果想长期存活,一定会做持久化,否则重启一下就没了。常见的持久化手段无非是定时任务、开机启动项、系统服务、shell 初始化文件。依次排查这几个位置:
crontab -l ls -la /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/ 2>/dev/null systemctl list-unit-files | grep -iE "litellm|python|tmp|curl" grep -nE "litellm|curl|wget|base64 -d" ~/.bashrc ~/.profile ~/.zshrc 2>/dev/null先看当前用户的 crontab,再看系统的 cron 目录,然后看 systemd 服务里有没有可疑的 unit,最后是 shell 初始化文件。攻击者很喜欢把“下载脚本、执行脚本”一类的命令写进.bashrc,这样每次你开一个终端,恶意代码就会悄悄执行一次。
这里也有一个很多人踩过的坑:只看当前用户的 crontab,忽略了 root 的 crontab。如果投毒代码提权成功,它会把自己写进 root 的计划任务里。所以排查的时候,sudo crontab -l也要看,除非你有把握当前 shell 一定是干净环境,否则这一步不能省。
另外,启动项目录和 systemd 支持的用户级服务也值得扫一眼:
ls -la ~/.config/systemd/user/ 2>/dev/null ls -la /etc/systemd/system/ | grep -iE "python|tmp|update|sync"看到名字可疑的服务,别急着删,先systemctl cat 服务名看它的 ExecStart 里到底执行了什么。有时候 ExecStart 会指向/tmp下的脚本,这个位置本身就非常值得怀疑。
2.4 第四步:检查网络连接和日志异常
如果前面几步都没有发现明显问题,不代表就彻底干净了,还要看日志。litellm 在代理模式下默认会把请求日志打到 stdout,如果配置了日志文件,就找一下这些文件:
find /var/log /tmp ~/.cache -name "*litellm*" 2>/dev/null日志里最值得关注的是:有没有发往陌生端点的请求。正常的 litellm 请求应该只发给你配置过的模型服务商,比如 OpenAI、Anthropic、Azure 的域名。如果日志里出现一个从没见过的域名或者 IP,而你的配置文件里根本没有这个地址,那基本可以断定网关已经被劫持了。
此外,可以检查 Python 环境里的 sitecustomize.py 和 usercustomize.py。这两个文件一存在就会被 Python 自动导入,是投毒代码非常喜欢的“隐藏位”。查一下 site-packages 根目录以及用户 site 目录里有没有这两个文件,有的话打开看看内容,正常的第三方包不会往这两个文件里塞东西:
python -m site find /usr/local/lib/python3.*/site-packages -maxdepth 1 -name "*customize.py" 2>/dev/null find ~/.local/lib/python3.*/site-packages -maxdepth 1 -name "*customize.py" 2>/dev/null到这里,机器层面的排查基本就完整了。如果全都没问题,说明你的机器大概率是干净的;如果中途发现了可疑的东西,先不要急着杀进程或删文件,记住“隔离、取证、轮换密钥”的顺序,后面第四节详细讲。
3. 验证环境无异常后的加固:litellm proxy 最佳实践
3.1 部署层面:固定版本与签名校验
排查看不出问题,只能说明“目前”是干净的。投毒是一场比赛,攻击者不断换手法,我们要做的就是尽量别做最后一个升级的用户。在这方面,litellm proxy 的部署有几个基础但特别管用的原则。
第一个原则是永远不要用latest标签。无论是 pip 安装还是 Docker 拉镜像,都不要图省事装最新版。正确做法是锁定到一个明确的小版本,升级时先看 changelog、看 GitHub release 说明,确认没有异常后再动。比如 Docker 部署不要这么写:
FROM ghcr.io/berriai/litellm:latest而是写成这样:
FROM ghcr.io/berriai/litellm:main-v1.52.1更稳妥的做法是直接用镜像的 digest 锁定不可变内容。Digest 是镜像内容的哈希值,只要镜像内容变了,digest 就会变,相当于给镜像加了一个防伪标识。如果你所在的公司对安全要求比较高,建议镜像引用的就是这个 digest 而不是 tag。
pip 安装也有类似的锁定方式。如果你们用 requirements.txt 管理依赖,强烈建议把哈希校验也加上。格式是这样的:
litellm==1.52.1 --hash=sha256:xxxxxxx哈希值可以从 PyPI 页面直接复制。这样即使攻击者把 PyPI 上的某个版本替换成恶意包,只要哈希不匹配,pip 安装就会直接失败,不会让脏东西混进来。
第二个原则是尽量从官方渠道获取包。pip 安装就走 PyPI,Docker 就走 ghcr.io 官方仓库,GitHub 源码就走官方仓库的 tag。不要在非官方博客、网盘、QQ 群里下载“安装包”或“镜像”,这个坑踩一次就够受的。
3.2 配置层面:最小权限与模型白名单
litellm proxy 的配置文件是核心。很多人安装完就用默认配置直接跑,什么模型都允许、什么 key 都能调用,这样做确实方便,但风险极大。正确做法是把配置做成“默认拒绝,显式允许”。下面是一个基础但相对安全的 config.yaml 示例:
model_list: - model_name: "gpt-4o" litellm_params: model: "openai/gpt-4o" api_key: "os.environ/OPENAI_API_KEY" - model_name: "claude-3-5-sonnet" litellm_params: model: "anthropic/claude-3-5-sonnet" api_key: "os.environ/ANTHROPIC_API_KEY" litellm_settings: drop_params: true set_verbose: true num_retries: 1 request_timeout: 30 general_settings: master_key: "os.environ/LITELLM_MASTER_KEY" database_url: "os.environ/DATABASE_URL"有几个关键点:所有 api_key 都通过os.environ/引用环境变量,而不是直接写在 YAML 里。这样即便配置文件泄露,密钥也不会跟一起泄露。master_key是管理端口的访问密钥,一定不要用默认值。model_list里只列团队实际用到的模型,没有列出来的模型,别人就算拿到了代理地址也调不动。
启动时指定配置:
litellm --config /path/to/config.yaml --port 4000还有一个小技巧:litellm 支持用--num_retries控制重试次数,从安全角度我建议把重试次数设成 1,不要设成无限重试。投毒代码如果伪装成上游模型服务异常,无限重试会让网关长时间占用连接、持续请求同一个恶意端点,既影响排查也扩大攻击面。
3.3 运行层面:密钥管理、日志审计与灰度升级
运行过程中的安全,核心是两件事:密钥管理和日志审计。litellm 会接触所有上游模型的 API key,这些 key 一旦泄露,损失可能远超想象。所以密钥要尽量做到“短时有效、随用随换”,不要一把 key 用一年。现在各大云厂商都支持子账号和多密钥轮换,建议给 litellm 单独建一个专用 key,权限范围只限定它转发模型所使用的产品线,不要把管理员权限给它。
日志审计上,至少要做到两点:一是保留最近 90 天的请求日志,二是定期扫描日志中的异常行为。异常行为包括:某个 key 突然在短时间内发起大量请求、请求的模型不在白名单列表里、响应内容里出现了奇怪的长文本、凌晨三四点有非预期的调用高峰。这些都可以通过简单的脚本或者日志分析平台做告警,不需要多复杂的模型,规则里写清楚就能拦住大多数投毒痕迹。
升级方面,我的习惯是“小版本跟上,大版本观察两天”。安全修复通常出现在小版本里,这种要尽快跟上;大版本改动多,先看 release note 再决定要不要升。另外,如果你用 Docker 部署,升级前可以先在本地把镜像拉下来,用docker history看一下镜像构建过程,确认没有可疑的 RUN 指令,再推送到生产环境。
4. 从包到模型:大模型投毒与标签投毒的边界
4.1 标签投毒到底是什么
litellm 这类基础设施被投毒只是供应链安全的一部分,还有一个话题在圈子里被越来越频繁地提及——大模型投毒,更准确地说,是训练数据层面的标签投毒。所谓标签投毒,指的是攻击者在模型训练或微调阶段,故意篡改数据集里的标签,把输入的样本指向错误的期望输出。
举个例子:一个用于识别客服语气的情感分类模型,攻击者把大量“客户投诉”样本的标签改成“正常咨询”,模型学完之后就学歪了,会把真正的投诉也当正常处理。在生成式模型里,标签投毒的表现更隐蔽,比如在指令微调数据中混入几百条“当用户提到某个关键词时,输出固定的一段错误信息”,模型很难被发现哪里出了问题,但行为已经偏离了预期。
标签投毒的可怕之处在于它不依赖传统漏洞。代码层面你可能做了所有安全检查,漏洞扫描、依赖更新、进程监控全都正常,但模型本身携带了恶意行为,这个行为只有在特定输入触发时才会暴露。很多团队现在做的所谓“大模型投毒测试”,本质上就是设计一堆触发样本去探测模型是否存在异常行为,但它能覆盖的攻击面其实很有限。
那这跟 litellm 有什么关系?很简单,litellm 是模型请求的入口,它决定了你的流量会转发到哪个模型、哪个服务商。如果攻击者在 litellm 层面做了手脚,把原本发往合法模型的请求悄悄改发到另一个被投毒的模型上,那你表面看是调了一个安全模型,实际上可能收到的全是恶意模型返回的内容。这就是为什么这一节要放在一起讲:网关安全和模型安全是串联的,任何一环被突破,整个链路都不可信。
4.2 网关侧如何做基础防线
在 litellm proxy 的配置里,一个容易被忽略但非常关键的能力是 model 映射。你可以通过配置把外部的模型名映射到内部真正的目标模型上,比如:
model_list: - model_name: "internal-safe-llm" litellm_params: model: "openai/gpt-4o" api_key: "os.environ/OPENAI_API_KEY"这样团队内部只知道internal-safe-llm这个逻辑名字,根本不需要知道背后的实际模型是什么。好处是,万一某个模型被发现存在安全风险,管理员只需要改一行配置,把internal-safe-llm指向替代模型,而不需要通知所有调用方改代码。反过来,如果攻击者想在网关层做手脚,他能利用的入口也比直接配置明文模型名少得多。
另一个防线是对输出做基础校验。litellm 本身不是一个安全防护工具,但你可以前置一道简单的检查逻辑,比如对返回内容做关键词过滤,或者对请求体做格式校验。更复杂一点的团队会在 litellm 前面再放一层专门的网关服务,统一做注入检测、敏感内容拦截、速率控制。这种做法本质上就是把安全能力跟模型网关解耦,litellm 专心做转发,安全审核交给专门的服务。
不要忽略 prompt injection 的威胁。用户输入里可能夹带“忽略之前所有指令”之类的攻击语句,如果应用直接把模型返回内容拼进系统指令里,就可能形成二次注入。这里的原则是:模型输出永远视为不可信数据。任何模型返回的内容,必要的时候都要做过滤、转义、人工抽检,而不是直接拿来当代码执行、拼 SQL 或者写文件。
5. 应急处理与长期防护建议
5.1 如果真的中招,按这个顺序处理
如果你在排查中真的发现了可疑的东西,不要慌,更不要立刻把机器重启或者直接杀进程。正确的应急顺序是:隔离、取证、轮换、重建。
第一步是隔离。把可疑机器的网络断开,拔掉网线、iptables 阻断外联都可以。这一步的目的是防止恶意代码继续把数据外传,也防止攻击者通过这台机器横向移动到内网其他服务器。注意,隔离之前先记录一下当前进程列表和网络连接,这些都是后续分析的关键现场。
第二步是取证。把可疑的进程、文件、cron 任务、shell 历史、日志都截图或拷贝到外部存储。取证不需要多专业,关键是别破坏原始数据,别在取证之前跑去“手动清理”可疑文件。很多时候你以为删掉了一个恶意脚本,实际上攻击者还有一个隐藏副本没被你找到,你一删除,反而打草惊蛇。
第三步是轮换密钥。这是最不能拖的一步。只要机器有被攻破可能,所有在这台机器上出现过的密钥都算泄露,包括但不限于 litellm 转发过的各家模型 API key、数据库密码、内部系统的 token。挨个重新生成,别心疼那点时间,密钥不轮换,就永远别睡安稳觉。
第四步是重建。把系统重装、容器重新拉取镜像、代码从干净仓库重新部署,确保环境从可信基线开始。如果你有配置管理和基础设施即代码的习惯,这一步会很快,否则只能手动做,很痛苦,但躲不掉。
5.2 长期防护的几条硬规则
投毒这件事,防住一次不难,难在持续防住。分享几条我一直坚持的硬规则,不一定全面,但每一条都是踩过坑后总结出来的:
第一,依赖锁定是底线。Python 项目必须锁定 lock 文件,最好连传递依赖的哈希也一起锁。没有锁文件的依赖管理,本质上就是开着门让别人往里随便搬东西。
第二,运行时权限最小化。litellm 不需要 root 权限,就不要用 root 跑。创建一个单独的运行用户,给它最小的目录写入权限、最小的网络访问范围,容器里只保留运行必需的软件包。很多投毒代码设计得很精巧,但它在最小权限环境下根本跑不起来。
第三,部署环境要跟开发环境隔离。开发机上装什么依赖、跑什么脚本都是低风险的事,但生产环境必须走固定流程,手动在服务器上敲 install 这种事能免则免。
第四,安全扫描进 CI。现在有各种依赖漏洞扫描工具,把它们接进 CI 流程,每次提交、每次构建都自动跑一遍。不要等到安全公告爆出来才想起来检查,日常的自动化扫描才能兜住底。
最后一条,建立异常行为告警。litellm 的请求如果出现了非预期的模型名、非预期的响应长度、非预期的服务商,都应该触发告警。攻击者往往很安静,但他们的行为模式总会露出一两个异常的角。
写在最后
这次 litellm 投毒风波,不管最后定性是官方包被污染、还是仿冒包钓鱼,核心问题已经摆到台面上:模型网关这种基础设施,一旦被污染就是全局事故。我个人在实际排查中最大的体会是,投毒最难的地方不是“识别单个可疑文件”,而是“建立一套可复用的核查流程”。你如果每次遇到风声都要从头想该查哪里,那大概率会漏;但如果你有一套固定的检查清单,从包来源、进程、端口、持久化、日志一路查下来,反而十分钟就能给出结论。
最后再分享一个小技巧:把上面这些检查命令写成一个 shell 脚本,放在团队的部署文档里,每次升级 litellm 或者出现安全传闻时,大家直接跑一遍脚本生成报告,而不是各自在终端里人肉敲命令。我试过用同样思路处理过几次其他开源组件的安全告警,省下的时间非常可观。安全这块,自动化永远比自觉可靠。