--no-plugins是必须加在每条命令末尾的安全硬开关,它强制跳过所有插件加载与执行,切断PluginInterface::activate()调用链,防止恶意插件在install/update时静默运行任意代码。

不能靠“不下载”来规避恶意插件——插件代码早已随包一起下载完毕,真正危险的是安装/更新时的自动执行。 插件不是运行时才拉取的远程资源,而是随 composer install 解压到 vendor/ 后立即参与构建流程。所谓“禁止自动下载”本身是个伪命题,关键在阻断执行链。
为什么 --no-plugins 不是可选,而是必须加在每条命令末尾
Composer 2.2+ 默认拒绝插件,但只要命令里漏掉 --no-plugins,它就会照常扫描 vendor/、读取 composer.json 中的 plugins 字段、调用 PluginInterface::activate()——哪怕你没显式配置 allow-plugins,只要依赖树里有带插件的包(比如旧版 hirak/prestissimo 或 fork 的 phpunit/phpunit),它就可能静默运行。
-
composer install --no-plugins✅ 正确:跳过整个插件生命周期,连 autoload_plugins.php 都不会生成 -
composer --no-plugins install❌ 错误:等价于composer --help,参数被忽略 - CI 脚本若封装了
bin/composer,必须确认该脚本未默认注入--plugins或其他 flag - 环境变量
COMPOSER_NO_PLUGINS=1可作备用,但优先级低于命令行参数,且部分旧镜像未启用
allow-plugins 白名单怎么配才真正生效
allow-plugins 是唯一可控的插件授权开关,写在 composer.json 的 config 下,不是全局配置。设为 true 就等于放弃所有防御——它会绕过白名单机制,让所有包(包括传递依赖)无需校验即可执行代码。
- 合法写法只有一种:
{"config": {"allow-plugins": {"symfony/flex": true, "phpstan/extension-installer": true, "*": false}}} -
"*": false是最小化起点,必须显式列出每个你真需要的插件全名,大小写敏感 -
"vendor/*"或"*/*"这类通配符不被识别,会被 Composer 忽略 -
"symfony/flex": "true"(字符串)会导致 JSON 解析失败,必须用布尔值true
如何验证插件是否真被禁用,而不是“看起来没报错”
终端没输出 ≠ 插件没运行。恶意插件常静默失败或只在特定条件激活。得看三个硬信号:
- 检查
vendor/composer/autoload_plugins.php:若为空文件或根本不存在,说明插件未注册;若含非空数组,说明已加载 - 运行
composer diagnose --verbose,末尾搜索Loading plugin或Activating plugin——出现任一即表示失效 - CI 流程中加
composer audit --format=json | jq '.advisories | length',让漏洞数 >0 时直接失败
--no-plugins 只管插件,但 scripts 钩子仍可执行
--no-plugins 不影响 post-install-cmd 等 scripts 钩子。而很多恶意行为正是通过这些钩子实现的,比如写入 webshell、调用 exec()、curl 远程 payload。
- 生产环境必须组合使用:
composer install --no-plugins --no-scripts --no-dev --prefer-dist - 仅清空
composer.json中的scripts字段不够可靠,得设为空数组:"post-install-cmd": [],不能用null或false -
--prefer-dist强制走压缩包而非源码,能规避 Git 分支被篡改、安装脚本被重写的风险 - root 用户运行
composer install会污染vendor/所有权,后续 www-data 无法写入,部署直接失败
最易被忽略的一点:--no-plugins 禁用后,Symfony Flex 的 recipes 注入、PHP_CodeSniffer 的自动配置等深度集成行为会静默消失——这不是异常,是设计使然。别把它当成 bug 去修复,而是意识到:你主动切断了执行链,代价就是某些便利性功能不再自动生效。











