Composer Security Hardening
Composer is a dependency manager for PHP, and is the de facto one. It is a great software developed by brilliant people, and Composer has many security precautions in place to prevent several threat models such as supply chain attacks, HTTPS downgrade attacks, and offers features to further tighten security.
Composer 2, released October 2020, has even more security features in place to further limit the attack surface even at a worst-case scenario. Recent news such as an attacker injecting malicious code to event-stream dependencies, and the more recent news of a security researcher executing arbitrary code at several companies including Microsoft and Apple are making people aware on how important dependency managers have to be, and Composer is standing strong on that front.
Composer gets many things right when it comes to a dependency manager. Composer is a relatively new dependency manager compared to NPM and PIP, that gave Composer some inspiration on both good lessons and things to avoid when building a dependency manager for one of the most popular and widely used programming languages in the world.
Composer Packages are always vendor-namespaced
All Composer packages must be vendor-namespaced. This means Composer only allows packages such as "symfony/finder"
and "nette/finder"
, but never stand-alone package names such as "finder"
. This sets Composer far ahead of the competition such as NPM and PIP.
"Dependency confusion" attacks, such as the ones that affected Microsoft and Apple would not have happened with Composer because all packages must be vendor-namespaced, and Packagist (the online repository for packages) never allows a different account to claim a vendor name once a package is published under a vendor name.
symfony/
vendor name, for example, is reserved for the Symfony project, and nobody else can publish a Composer package with name symfony/*
to Packagist.
Security Precautions built-in to Composer/Packagist
Composer and Packagist provide many security precautions built-in. This includes the essentials such as requiring HTTPS, 2-factor authentication in Packagist, and entirety of Composer source code being open-source.
Lock files (composer.lock
) store absolute URLs to all packages used in a dependency tree, and guaranteed reproducible vendor directories.
Composer Packages are hosted on a VCS
Unlike NPM that hosts the actual code of versions, Packagist never stores the actual code. It merely stores the target URL and meta information about packages. Almost entirety of public composer packages submitted to Packagist are hosted on GitHub, GitLab, or a similar version controlling system.
This provides transparency, because the actual package contents can be reproduced and verified anytime.
Composer self-updates are cryptographically verified
Every time Composer is installed, or updated through composer self-update
command, the downloaded executable file is cryptographically verified for its integrity. If the integrity is not proved to be untampered with, Composer downright refuses to accept the downloaded Phar file.
Phive provides signature-verified Phar installations and updates for tools such as Composer, PHPUnit, Psalm, Phan, and several more.
Root-user warnings
Composer 1 had a feature that it detects if Composer is run as the root user of a Linux system, and emits a warning that it's a bad idea.
In Composer 2, it is leveled up to a prompt that the root user is warned, and must continue manually before the rest of the Composer command is executed.
Composer Repositories are Canonical by default
One of the most important changes in Composer 2 is that it considers a repository to be canonical; When using multiple repositories, if a package is found at a repository, Composer will not look for newer versions in the rest of the declared repositories.
This mitigates the threat when an attacker publishes a higher version of a given package and trick Composer to download the package from a repository the attacker potentially controls.
Additional Security Hardening
Using code from a third party implies trust and well-intent. It not only applies to Composer, but also pretty much every software being used on a system. Composer provides features such as vendor namespaces that provides some level of a trust chain, a secure channel (such as HTTPS and commit-hashes), and secure verified updates to Composer itself.
There are some additional security hardening that tighten the attack surface and provide safe-guards to a Composer project.
Block packages with known vulnerabilities
roave/security-advisories
is a composer meta-package that has no functional code by itself, but has a composer.json
file that prevents packages and package versions with known vulnerabilities from being installed.
composer require --dev roave/security-advisories:dev-latest
Running above, or adding roave/security-advisories
to require-dev
section in the composer.json
file in a project does the trick. Once installed, any packages with known vulnerabilities will be considered a conflict package version, and Composer will refuse to install the vulnerable package or the vulnerable version of one.
A Drupal-specific advisories list (that can be used in addition to roave/security-advisories
) and a WordPress list are also available.
Limit Repository Scopes
Composer supports configuring additional repositories to source packages. For example, Drupal, WordPress, Yii, and Magento are some example open-source projects that provide their own repository of packages. Composer would look for a given package name in all of these repositories, and then finally at Packagist.org looking for a package with a given name.
In Composer 2, canonical repository feature is introduced, that by default, Composer stops looking for package versions in other repositories if it finds the package in a repository. Packagist.com, from the very creators of Composer, provides a feature similar to canonical repositories for Composer version 1.x by acting as a mirror to packagist.org.
When using a repository for an open-source project, or a private repository such as from GitLab, it is possible to optionally limit the packages the repositories may host.
Following is a composer.json
snippet that adds Drupal and WordPress repositories.
{
"repositories":[
{
"type":"composer",
"url":"https://packages.drupal.org/8",
"only": ["drupal/*"]
},
{
"type":"composer",
"url":"https://wpackagist.org",
"only": ["wpackagist-plugin/*", "wpackagist-theme/*"]
}
],
}
The only
clause tells Composer to only look for drupal/*
packages in Drupal repository, and wpackagist*
packages in the WPackagist repository.
With the only
clause, Composer will not attempt to look for other packages (such as symfony/finder
) in those repositories. Even if an attacker manages to host a package in those repositories, Composer will not look for symfony/finder
in them.
Further, it is possible to exclude certain packages from a repository. The example below shows a repository in https://example.com
, that Composer can look for all packages except example/outdated-package
.
{
"repositories":[
{
"type":"composer",
"url":"https://example.com",
"exclude": ["example/outdated-package"]
}
],
}
Composer Scripts
By default, Composer runs scripts declared in the package being installed. This essentially means that when a package is installed, it is given a chance to immediately execute arbitrary code.
Laravel, for example has a scripts
section in its composer.json
file to complete the installation of Laravel to a usable state.
It is a good idea to inspect the packages being installed with a dry-run, or with scripts disabled unless it's absolutely necessary.
Dry-run composer require
composer require laravel/laravel --dry-run
A dry-run will not make any changes, but show the package and dependency list being installed. --dry-run
option is supported require
, install
, update
, and remove
commands.
Run Composer with scripts disabled
Composer provides a --no-scripts
option in require
, install
, update
, remove
, create-project
, and dump-autoload
commands that prevents any scripts declared in the package's composer.json
file from being run.
Implicit Autoloads
It is worth noting that any installed, despite not being used, it not necessarily safe. Development tools such as PHPUnit might not be required for a production web site, and it is easy to forget to not use those development packages (require-dev
packages) in production.
Composer's autoloader is feature-rich, and it supports not only PSR-0 and PSR-4, but also classmap
and files
options. symfony/polyfill
for example uses the files
option to always include files that it polyfills. If the package makes it to the autoloader, all files declared under autoload.files
section of all packages' composer.json
files are automatically included, whether they are used or not.
When deploying to production, make sure to not include require-dev
packages with the --no-dev
option.
composer install --no-dev
This command installs a Composer project using an existing composer.lock
file, but does not fetch or include autoload entries from require-dev
packages.
composer dump-autoload --no-dev
It is also possible to generate the autoloader without including the require-dev
packages' autoload entries. It will not remove the packages, but simply exclude them from the autoloader.
Composer is a powerful tool for PHP dependency management, and it tries to provide the essentials to do its work securely. However, the chain of trust always has to root somewhere by the nature of secure software. Using a Composer package of a third party automatically implies trust on that vendor.
The steps above will provide protection against malicious third parties that might try to tamper the code at its manifest lookup or transit.