Disallow plugins by throwing an exception if non-interactive to avoid half-broken runtime states (#10920)
* Disallow plugins by throwing an exception if non-interactive to avoid half-broken runtime states, fixes #10912 * Also allow BC mode for lock files older than 2.2.0 to keep plugins working there * Allow locker to be accessed by plugin manager at init time * Update allow-plugins docs Co-authored-by: Damien Tournoud <damien@platform.sh> Co-authored-by: Jordi Boggiano <j.boggiano@seld.be>pull/10985/head
parent
f14b02b9c9
commit
92e1c26c3b
|
@ -52,7 +52,15 @@ and **false** to disallow while suppressing further warnings and prompts.
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also set the config option itself to `false` to disallow all plugins, or `true` to allow all plugins to run (NOT recommended).
|
You can also set the config option itself to `false` to disallow all plugins, or `true` to allow all plugins to run (NOT recommended). For example:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"allow-plugins": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## use-include-path
|
## use-include-path
|
||||||
|
|
||||||
|
|
|
@ -427,6 +427,17 @@ class Factory
|
||||||
// add installers to the manager (must happen after download manager is created since they read it out of $composer)
|
// add installers to the manager (must happen after download manager is created since they read it out of $composer)
|
||||||
$this->createDefaultInstallers($im, $composer, $io, $process);
|
$this->createDefaultInstallers($im, $composer, $io, $process);
|
||||||
|
|
||||||
|
// init locker if possible
|
||||||
|
if ($fullLoad && isset($composerFile)) {
|
||||||
|
$lockFile = self::getLockFile($composerFile);
|
||||||
|
if (!$config->get('lock') && file_exists($lockFile)) {
|
||||||
|
$io->writeError('<warning>'.$lockFile.' is present but ignored as the "lock" config option is disabled.</warning>');
|
||||||
|
}
|
||||||
|
|
||||||
|
$locker = new Package\Locker($io, new JsonFile($config->get('lock') ? $lockFile : Platform::getDevNull(), null, $io), $im, file_get_contents($composerFile), $process);
|
||||||
|
$composer->setLocker($locker);
|
||||||
|
}
|
||||||
|
|
||||||
if ($fullLoad) {
|
if ($fullLoad) {
|
||||||
$globalComposer = null;
|
$globalComposer = null;
|
||||||
if (realpath($config->get('home')) !== $cwd) {
|
if (realpath($config->get('home')) !== $cwd) {
|
||||||
|
@ -439,17 +450,6 @@ class Factory
|
||||||
$pm->loadInstalledPlugins();
|
$pm->loadInstalledPlugins();
|
||||||
}
|
}
|
||||||
|
|
||||||
// init locker if possible
|
|
||||||
if ($fullLoad && isset($composerFile)) {
|
|
||||||
$lockFile = self::getLockFile($composerFile);
|
|
||||||
if (!$config->get('lock') && file_exists($lockFile)) {
|
|
||||||
$io->writeError('<warning>'.$lockFile.' is present but ignored as the "lock" config option is disabled.</warning>');
|
|
||||||
}
|
|
||||||
|
|
||||||
$locker = new Package\Locker($io, new JsonFile($config->get('lock') ? $lockFile : Platform::getDevNull(), null, $io), $im, file_get_contents($composerFile), $process);
|
|
||||||
$composer->setLocker($locker);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($fullLoad) {
|
if ($fullLoad) {
|
||||||
$initEvent = new Event(PluginEvents::INIT);
|
$initEvent = new Event(PluginEvents::INIT);
|
||||||
$composer->getEventDispatcher()->dispatch($initEvent->getName(), $initEvent);
|
$composer->getEventDispatcher()->dispatch($initEvent->getName(), $initEvent);
|
||||||
|
|
|
@ -47,6 +47,19 @@ class PluginInstaller extends LibraryInstaller
|
||||||
return $packageType === 'composer-plugin' || $packageType === 'composer-installer';
|
return $packageType === 'composer-plugin' || $packageType === 'composer-installer';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null)
|
||||||
|
{
|
||||||
|
// fail install process early if it going to fail due to a plugin not being allowed
|
||||||
|
if ($type === 'install' || $type === 'update') {
|
||||||
|
$this->composer->getPluginManager()->isPluginAllowed($package->getName(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parent::prepare($type, $package, $prevPackage);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -318,6 +318,16 @@ class Locker
|
||||||
return isset($lockData['aliases']) ? $lockData['aliases'] : array();
|
return isset($lockData['aliases']) ? $lockData['aliases'] : array();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getPluginApi()
|
||||||
|
{
|
||||||
|
$lockData = $this->getLockData();
|
||||||
|
|
||||||
|
return isset($lockData['plugin-api-version']) ? $lockData['plugin-api-version'] : '1.1.0';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -18,6 +18,7 @@ use Composer\Installer\InstallerInterface;
|
||||||
use Composer\IO\IOInterface;
|
use Composer\IO\IOInterface;
|
||||||
use Composer\Package\BasePackage;
|
use Composer\Package\BasePackage;
|
||||||
use Composer\Package\CompletePackage;
|
use Composer\Package\CompletePackage;
|
||||||
|
use Composer\Package\Locker;
|
||||||
use Composer\Package\Package;
|
use Composer\Package\Package;
|
||||||
use Composer\Package\Version\VersionParser;
|
use Composer\Package\Version\VersionParser;
|
||||||
use Composer\Pcre\Preg;
|
use Composer\Pcre\Preg;
|
||||||
|
@ -82,9 +83,8 @@ class PluginManager
|
||||||
$this->globalComposer = $globalComposer;
|
$this->globalComposer = $globalComposer;
|
||||||
$this->versionParser = new VersionParser();
|
$this->versionParser = new VersionParser();
|
||||||
$this->disablePlugins = $disablePlugins;
|
$this->disablePlugins = $disablePlugins;
|
||||||
|
$this->allowPluginRules = $this->parseAllowedPlugins($composer->getConfig()->get('allow-plugins'), $composer->getLocker());
|
||||||
$this->allowPluginRules = $this->parseAllowedPlugins($composer->getConfig()->get('allow-plugins'));
|
$this->allowGlobalPluginRules = $this->parseAllowedPlugins($globalComposer !== null ? $globalComposer->getConfig()->get('allow-plugins') : false, $globalComposer !== null ? $globalComposer->getLocker() : null);
|
||||||
$this->allowGlobalPluginRules = $this->parseAllowedPlugins($globalComposer !== null ? $globalComposer->getConfig()->get('allow-plugins') : false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -653,12 +653,12 @@ class PluginManager
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, bool>|bool|null $allowPluginsConfig
|
* @param array<string, bool>|bool $allowPluginsConfig
|
||||||
* @return array<non-empty-string, bool>|null
|
* @return array<non-empty-string, bool>|null
|
||||||
*/
|
*/
|
||||||
private function parseAllowedPlugins($allowPluginsConfig)
|
private function parseAllowedPlugins($allowPluginsConfig, Locker $locker = null)
|
||||||
{
|
{
|
||||||
if (null === $allowPluginsConfig) {
|
if (array() === $allowPluginsConfig && $locker !== null && $locker->isLocked() && version_compare($locker->getPluginApi(), '2.2.0', '<')) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -679,22 +679,28 @@ class PluginManager
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
* @param string $package
|
* @param string $package
|
||||||
* @param bool $isGlobalPlugin
|
* @param bool $isGlobalPlugin
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
private function isPluginAllowed($package, $isGlobalPlugin)
|
public function isPluginAllowed($package, $isGlobalPlugin)
|
||||||
{
|
{
|
||||||
static $warned = array();
|
if ($isGlobalPlugin) {
|
||||||
$rules = $isGlobalPlugin ? $this->allowGlobalPluginRules : $this->allowPluginRules;
|
$rules = &$this->allowGlobalPluginRules;
|
||||||
|
} else {
|
||||||
|
$rules = &$this->allowPluginRules;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a BC mode for lock files created pre-Composer-2.2 where the expectation of
|
||||||
|
// an allow-plugins config being present cannot be made.
|
||||||
if ($rules === null) {
|
if ($rules === null) {
|
||||||
if (!$this->io->isInteractive()) {
|
if (!$this->io->isInteractive()) {
|
||||||
if (!isset($warned['all'])) {
|
|
||||||
$this->io->writeError('<warning>For additional security you should declare the allow-plugins config with a list of packages names that are allowed to run code. See https://getcomposer.org/allow-plugins</warning>');
|
$this->io->writeError('<warning>For additional security you should declare the allow-plugins config with a list of packages names that are allowed to run code. See https://getcomposer.org/allow-plugins</warning>');
|
||||||
$this->io->writeError('<warning>You have until July 2022 to add the setting. Composer will then switch the default behavior to disallow all plugins.</warning>');
|
$this->io->writeError('<warning>This warning will become an exception once you run composer update!</warning>');
|
||||||
$warned['all'] = true;
|
|
||||||
}
|
$rules = array('{}' => true);
|
||||||
|
|
||||||
// if no config is defined we allow all plugins for BC
|
// if no config is defined we allow all plugins for BC
|
||||||
return true;
|
return true;
|
||||||
|
@ -714,24 +720,20 @@ class PluginManager
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isset($warned[$package])) {
|
|
||||||
if ($this->io->isInteractive()) {
|
if ($this->io->isInteractive()) {
|
||||||
$composer = $isGlobalPlugin && $this->globalComposer !== null ? $this->globalComposer : $this->composer;
|
$composer = $isGlobalPlugin && $this->globalComposer !== null ? $this->globalComposer : $this->composer;
|
||||||
|
|
||||||
$this->io->writeError('<warning>'.$package.($isGlobalPlugin ? ' (installed globally)' : '').' contains a Composer plugin which is currently not in your allow-plugins config. See https://getcomposer.org/allow-plugins</warning>');
|
$this->io->writeError('<warning>'.$package.($isGlobalPlugin ? ' (installed globally)' : '').' contains a Composer plugin which is currently not in your allow-plugins config. See https://getcomposer.org/allow-plugins</warning>');
|
||||||
while (true) {
|
while (true) {
|
||||||
switch ($answer = $this->io->ask('Do you trust "<info>'.$package.'</info>" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [<comment>y,n,d,?</comment>] ', '?')) {
|
switch ($answer = $this->io->ask('Do you trust "<info>'.$package.'</info>" to execute code and wish to enable it now? (writes "allow-plugins" to composer.json) [<comment>y,n,d,?</comment>] ',
|
||||||
|
'?')) {
|
||||||
case 'y':
|
case 'y':
|
||||||
case 'n':
|
case 'n':
|
||||||
case 'd':
|
case 'd':
|
||||||
$allow = $answer === 'y';
|
$allow = $answer === 'y';
|
||||||
|
|
||||||
// persist answer in current rules to avoid prompting again if the package gets reloaded
|
// persist answer in current rules to avoid prompting again if the package gets reloaded
|
||||||
if ($isGlobalPlugin) {
|
$rules[BasePackage::packageNameToRegexp($package)] = $allow;
|
||||||
$this->allowGlobalPluginRules[BasePackage::packageNameToRegexp($package)] = $allow;
|
|
||||||
} else {
|
|
||||||
$this->allowPluginRules[BasePackage::packageNameToRegexp($package)] = $allow;
|
|
||||||
}
|
|
||||||
|
|
||||||
// persist answer in composer.json if it wasn't simply discarded
|
// persist answer in composer.json if it wasn't simply discarded
|
||||||
if ($answer === 'y' || $answer === 'n') {
|
if ($answer === 'y' || $answer === 'n') {
|
||||||
|
@ -751,13 +753,12 @@ class PluginManager
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
$this->io->writeError('<warning>'.$package.($isGlobalPlugin ? ' (installed globally)' : '').' contains a Composer plugin which is blocked by your allow-plugins config. You may add it to the list if you consider it safe. See https://getcomposer.org/allow-plugins</warning>');
|
|
||||||
$this->io->writeError('<warning>You can run "composer '.($isGlobalPlugin ? 'global ' : '').'config --no-plugins allow-plugins.'.$package.' [true|false]" to enable it (true) or keep it disabled and suppress this warning (false)</warning>');
|
|
||||||
}
|
|
||||||
$warned[$package] = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
throw new \UnexpectedValueException(
|
||||||
|
$package.($isGlobalPlugin ? ' (installed globally)' : '').' contains a Composer plugin which is blocked by your allow-plugins config. You may add it to the list if you consider it safe.'.PHP_EOL.
|
||||||
|
'You can run "composer '.($isGlobalPlugin ? 'global ' : '').'config --no-plugins allow-plugins.'.$package.' [true|false]" to enable it (true) or disable it explicitly and suppress this exception (false)'.PHP_EOL.
|
||||||
|
'See https://getcomposer.org/allow-plugins'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue