tl;dr There was a remote code execution vulnerability on packagist.org, the default package server behind Composer, a PHP package manager. Packagist currently serves around 400 million package downloads per month.
This bug was not technically interesting, but I figured it was worth posting about since Packagist appears to be a pretty popular service and is one of the top Google search results for “PHP package manager”.
You could type
$(execute me) into a big text field on the site and it would execute your command in a shell (twice).
You upload packages to Packagist by providing a URL to a Git, Perforce, Subversion, or Mercurial repository. To identify what kind of repository the URL points to, Packagist shells out to
hg, with application-specific commands that include this URL as an argument.
ProcessExecutor::execute, the method used to run shell commands, to print what commands it was being asked to execute. For a URL of
$(sleep 1), it would run the following:
git ls-remote --heads '$(sleep 1)' hg identify '$(sleep 1)' p4 -p $(sleep 1) info -s svn info --non-interactive $(sleep 1)
svn wrappers were improperly escaping the URL parameter.
The Packagist team quickly resolved this issue by escaping the relevant parameters in the Composer repository. I reported the vulnerability to
Package manager security is not always great, and you should probably plan on your package manager servers being compromised in the future.
In the past year or so I have found bugs that let me execute arbitrary code on rubygems.org, execute code on some of npm’s official mirrors (not the main registry), delete arbitrary release files from PyPI, serve arbitrary JS on every site using a popular CDN for npm, and now execute arbitrary code on packagist.org. Others are finding similar bugs. And that’s not even including the risk of a legitimate package being compromised.
In particular, I think it is a security anti-pattern to have application build pipelines pull fresh downloads of packages from upstream servers on every build if the packages are not expected to change. If for some reason you have to do this, you should pin dependencies using a cryptographically secure hash function.