必须为私有包添加作用域前缀(如@acme/)并禁用packagist回退,否则同名裸包易被攻击者劫持;需配置私有源优先、关闭公共源、固化vendor哈希、锁死版本且禁止生产环境执行composer命令。

生产环境跑着 Laravel,但 composer install 还在用公共 Packagist 源拉包?那不是“防不防依赖混淆”的问题,而是“哪天中招”的问题——尤其当你内部有未加作用域的私有包名(比如 laravel-utils、company-api-client)时,攻击者早就在 npm、Packagist 上发布了 1.99.0 版本等着你 composer update 了。
检查你的私有包是否已暴露在公共源上
依赖混淆的前提是:你用了同名但无作用域的包。Laravel 本身不强制作用域,但你的私有包如果没加 @yourcompany 或类似前缀,就等于把名字白送给攻击者。
- 运行
composer show --tree | grep -v "laravel/" | grep -v "illuminate/",筛出所有非官方依赖,逐个确认是否属于你团队维护、且是否带作用域(如@acme/laravel-utils) - 查
composer.json中的repositories配置:如果只写了"type": "composer", "url": "https://packagist.org",没配私有源,说明你根本没启用私有仓库优先级 - 重点盯
name字段:如果你的私有包composer.json里写的是"name": "acme/laravel-utils"(没@),它在 Packagist 上就是可被注册的——而攻击者已经批量注册过acme/*类命名空间
强制 Composer 优先走私有源并禁用公共回退
默认行为是“找不到私有包才查公共源”,但很多配置漏掉了关键开关,导致一查就 fallback 到 Packagist。
- 在项目根目录的
.composer/config.json或全局~/.composer/config.json中,必须设置:"secure-http": true(禁用 http 源)、"disable-tls": false -
composer.json的repositories必须显式声明私有源,并加"packagist.org": false关闭公共源:
"repositories": [
{
"type": "composer",
"url": "https://npm.yourcompany.com"
},
{
"packagist.org": false
}
]
- 验证是否生效:运行
composer config repos,输出里不能出现packagist.org;再执行composer show acme/laravel-utils,应报错“Package not found”,而不是成功显示一个来自 packagist.org 的高版本
CI/CD 构建阶段必须校验 vendor 目录完整性
就算你本地配对了,CI 流水线若用默认 composer install,仍可能从镜像源拉到被投毒的 zip 包——2026 年多起事件都是 CI 机器因缓存或镜像劫持下载了篡改过的 dist 包。
- 构建开始前,先生成 vendor 哈希快照:
find vendor -type f -name "*.php" -exec sha256sum {} \; | sort | sha256sum > vendor.sha256 - 部署后,在生产机上重新计算并比对:
sha256sum -c vendor.sha256,失败则立即中止启动 - 禁止在生产机执行任何
composer install或composer update;所有 vendor 必须由 CI 打包进不可变镜像(Docker 镜像或 tar.gz),解压即用 - 若用 GitHub Actions,记得在
actions/checkout@v4后加一步:composer install --no-dev --prefer-dist --optimize-autoloader,再立刻固化哈希
为什么 “minimum-stability”: “stable” 不够用
这个配置只能拦住 dev-master,但拦不住攻击者发布的 v1.99.0 —— 它是 stable 版本,语义化版本号还更高。
- 真实攻击样本(如 2026 年
lara-helper)都打着1.0.0、2.1.0标签发布,完全符合 stable 规则 -
prefer-stable: true只影响“多个稳定版共存时选哪个”,不阻止“只存在一个高版本稳定版”的场景 - 真正有效的约束是:在
composer.json里为每个私有包写死版本 + 禁用更新,例如:"acme/laravel-utils": "dev-main as 1.0.0",配合composer.lock提交,并在 CI 中校验 lock 文件未被修改
最易被忽略的一点:私有包的 composer.json 里 name 字段一旦写成无作用域格式(acme/utils 而非 @acme/utils),你就永远无法靠配置堵住这个口子——只能重命名、发新包、全量替换引用。别指望“暂时没被注册就安全”,2026 年已有自动化工具在持续扫描 GitHub 代码库提取这类裸名。











