In my project I check in the composer.lock file on github. Suppose I require a healthy dependency in the composer.json like:
"require": {
"foo/bar": "v3.0"
},
After I call composer install a composer.lock file is created.
"packages": [
{
"name": "foo/bar",
"version": "v3.0",
"source": {
"type": "git",
"url": "https://github.com/foo/bar.git",
"reference": "bbafb0edb791b23220563d113d00371ea42aedaa"
},
"type": "project",
"license": [
"MIT"
],
"authors": [
{
"name": "Mr.Foo",
"email": "mr.foo@bar.de"
}
],
"time": "2019-09-30T12:13:55+00:00"
}
Suppose an attacker owning the foo/bar repository would delete the v3.0 tag. The attacker would than name a different commit to v3.0. Can someone confirm that composer install will always check composer.lock installing dependencies? If I run a composer install without a composer.lock file composer will create a new .lock file with a new reference (commit id). If I run composer install with a composer.lock file composer will stick to the commit id ("reference": "bbafb0edb791b23220563d113d00371ea42aedaa" , old v3.0). Composer would not load the malicious fake v3.0. The v3.0 points to a new commit id on github.
Can someone confirm that composer.lock`s reference tag has a "higher priority" than the version tag? Does composer protects my project completely from those kinds of attacks?
Definitely, the answer to your question is:
Yes, composer will protect you
Either it will install the packages based on the commit hash stated in your composer.lock
, if it exists on the repository, just ignoring the mismatch between the commit and the version, either it will fail with a pretty straight forwardly expressed reason: "history was rewritten?"
The question did pique my curiosity: I would have said yes, because otherwise, locking the commit hash in the lock file would be useless, but I had to test it for the sake of correctness.
So here is what I did:
composer.json
constraintI installed a basic package, to some specific version (not the latest, just to have a version constraint):
$ composer require psr/log:1.0.0
Which made me end up with this pretty simple composer.json
{
"require": {
"psr/log": "1.0.0"
}
}
And this composer.lock
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "2865f724e23cffb23b3afd3a968e0359",
"packages": [
{
"name": "psr/log",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "fe0936ee26643249e916849d48e3a51d5f5e278b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b",
"reference": "fe0936ee26643249e916849d48e3a51d5f5e278b",
"shasum": ""
},
"type": "library",
"autoload": {
"psr-0": {
"Psr\\Log\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"keywords": [
"log",
"psr",
"psr-3"
],
"time": "2012-12-21T11:40:51+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
}
Then to test it, I just altered the commit hash fe0936ee26643249e916849d48e3a51d5f5e278b
everywhere I would find it in the composer.lock
by one character: fe0936ee26643249e916849d48e3a51d5f5e278c
(the last b
became a c
); ending with this composer.lock
:
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "2865f724e23cffb23b3afd3a968e0359",
"packages": [
{
"name": "psr/log",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "fe0936ee26643249e916849d48e3a51d5f5e278c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278c",
"reference": "fe0936ee26643249e916849d48e3a51d5f5e278c",
"shasum": ""
},
"type": "library",
"autoload": {
"psr-0": {
"Psr\\Log\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"keywords": [
"log",
"psr",
"psr-3"
],
"time": "2012-12-21T11:40:51+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
}
Note that: if you try this in your browser as composer would later do it for you, you will end up with a 404 page: https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278c
I removed my vendor
folder, for the sake of it:
$ rm -Rf vendor
Then, rerun a dependency installation, ending with this output:
$ composer install
Loading composer repositories with package information
Installing dependencies (including require-dev) from lock file
Package operations: 1 install, 0 updates, 0 removals
- Installing psr/log (1.0.0): Downloading (0%) Failed to download psr/log from dist: The "https://codeload.github.com/php-fig/log/legacy.zip/fe0936ee26643249e916849d48e3a51d5f5e278c" file could not be downloaded (HTTP/1.1 404 Not Found)
Now trying to download from source
- Installing psr/log (1.0.0): Cloning fe0936ee26 from cache
fe0936ee26643249e916849d48e3a51d5f5e278c is gone (history was rewritten?)
[RuntimeException]
Failed to execute git checkout 'fe0936ee26643249e916849d48e3a51d5f5e278c' -- && git reset --hard 'fe0936ee26643249e916849d48e3a51d
5f5e278c' --
fatal: reference is not a tree: fe0936ee26643249e916849d48e3a51d5f5e278c
install [--prefer-source] [--prefer-dist] [--dry-run] [--dev] [--no-dev] [--no-custom-installers] [--no-autoloader] [--no-scripts] [--no-progress] [--no-suggest] [-v|vv|vvv|--verbose] [-o|--optimize-autoloader] [-a|--classmap-authoritative] [--apcu-autoloader] [--ignore-platform-reqs] [--] [<packages>]...
If you only had one line to read out of this output, it would be:
fe0936ee26643249e916849d48e3a51d5f5e278c is gone (history was rewritten?)
This time, I did a little digging in the repository of php-fig/log to find the initial commit of the repository: https://github.com/php-fig/log/commit/a7ab552fdb2efb80aeca09da3bbd9335fc945ff0
And, in the same fashion, I edited my composer.lock
, but this time faking the fact that the initial commit of the repo is the one tagged 1.0.0
, when it is obviously not.
This got me this composer.lock
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "2865f724e23cffb23b3afd3a968e0359",
"packages": [
{
"name": "psr/log",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/log.git",
"reference": "a7ab552fdb2efb80aeca09da3bbd9335fc945ff0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/log/zipball/a7ab552fdb2efb80aeca09da3bbd9335fc945ff0",
"reference": "a7ab552fdb2efb80aeca09da3bbd9335fc945ff0",
"shasum": ""
},
"type": "library",
"autoload": {
"psr-0": {
"Psr\\Log\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Common interface for logging libraries",
"keywords": [
"log",
"psr",
"psr-3"
],
"time": "2012-12-21T11:40:51+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
}
Note that: trying it this time, would download the zip containing the repository state as it was at the initial commit: https://api.github.com/repos/php-fig/log/zipball/a7ab552fdb2efb80aeca09da3bbd9335fc945ff0
Repeated the above deletion of the vendor
folder
$ rm -Rf vendor
Cleared composer cache, also, this time, because, spoiler alert, the installation will succeed:
$ composer clearcache && rm -Rf vendor
Clearing cache (cache-vcs-dir): /tmp/cache/vcs
Clearing cache (cache-repo-dir): /tmp/cache/repo
Clearing cache (cache-files-dir): /tmp/cache/files
Clearing cache (cache-dir): /tmp/cache
All caches cleared.
Then, rerun a dependency installation, ending with this output:
$ composer install
Loading composer repositories with package information
Installing dependencies (including require-dev) from lock file
Package operations: 1 install, 0 updates, 0 removals
- Installing psr/log (1.0.0): Downloading (100%)
Generating autoload files
Curious about the installation going too well to my likening, I re-run the process, more verbosely, to know what composer was really doing:
$ rm -Rf vendor/ && composer clearcache && composer install -vvv
Cache directory does not exist (cache-vcs-dir):
Clearing cache (cache-repo-dir): /tmp/cache/repo
Clearing cache (cache-files-dir): /tmp/cache/files
Clearing cache (cache-dir): /tmp/cache
All caches cleared.
Reading ./composer.json
Loading config file ./composer.json
Checked CA file /etc/ssl/certs/ca-certificates.crt: valid
Executing command (/app): git branch --no-color --no-abbrev -v
Executing command (/app): git describe --exact-match --tags
Executing command (/app): git log --pretty="%H" -n1 HEAD
Executing command (/app): hg branch
Executing command (/app): fossil branch list
Executing command (/app): fossil tag list
Executing command (/app): svn info --xml
Failed to initialize global composer: Composer could not find the config file: /tmp/composer.json
To initialize a project, please create a composer.json file as described in the https://getcomposer.org/ "Getting Started" section
Running 1.8.6 (2019-06-11 15:03:05) with PHP 7.3.8 on Linux / 4.9.184-linuxkit
Reading ./composer.lock
Loading composer repositories with package information
Installing dependencies (including require-dev) from lock file
Reading ./composer.lock
Resolving dependencies through SAT
Looking at all rules.
Dependency resolution completed in 0.000 seconds
Analyzed 43 packages to resolve dependencies
Analyzed 43 rules to resolve dependencies
Package operations: 1 install, 0 updates, 0 removals
Installs: psr/log:1.0.0
- Installing psr/log (1.0.0): Downloading https://api.github.com/repos/php-fig/log/zipball/a7ab552fdb2efb80aeca09da3bbd9335fc945ff0
Downloading (connecting...)
Following redirect (2) https://codeload.github.com/php-fig/log/legacy.zip/a7ab552fdb2efb80aeca09da3bbd9335fc945ff0
Downloading https://codeload.github.com/php-fig/log/legacy.zip/a7ab552fdb2efb80aeca09da3bbd9335fc945ff0
Downloading (100%)Writing /tmp/cache/files/psr/log/6e79f232da13c50e0fd07e74eb2d58c350e71a60.zip into cache from /app/vendor/psr/log/4ff496e542e24af2efd56eaf051e132b
Extracting archiveExecuting command (CWD): unzip -qq '/app/vendor/psr/log/4ff496e542e24af2efd56eaf051e132b' -d '/app/vendor/composer/9c2feb29'
REASON: Required by the root package: Install command rule (install psr/log 1.0.0)
Generating autoload files
Where you can see it install the library at the commit hash a7ab552fdb2efb80aeca09da3bbd9335fc945ff0
, trusting the composer.lock
instructions to do so.
- Installing psr/log (1.0.0): Downloading https://api.github.com/repos/php-fig/log/zipball/a7ab552fdb2efb80aeca09da3bbd9335fc945ff0
There is a gotcha in the fact that, because the lock files says this commit is the version 1.0.0
it prompts me that it installed the package at that version, but this is a minor issue.