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”.

Vulnerability

You could type $(execute me) into a big text field on the site and it would execute your command in a shell (twice).

Packagist screenshot

WHY

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 git, p4, svn, and hg, with application-specific commands that include this URL as an argument.

I instrumented 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)

The p4 and svn wrappers were improperly escaping the URL parameter.

Mitigation

The Packagist team quickly resolved this issue by escaping the relevant parameters in the Composer repository. I reported the vulnerability to security@packagist.org.

Conclusion

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.