/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php Web access log Triage: medium Known exploited (CISA KEV)

eval-stdin.php Requests in Your Logs: Scanners Probing for CVE-2017-9841

Published

An automated probe for a leftover PHPUnit test-runner file under an exposed /vendor folder, which on unpatched versions turns POST data into PHP code the server executes.

What it looks like

203.0.113.7 - - [18/Oct/2026:03:51:11 +0000] "GET /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 404 162 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
198.51.100.42 - - [18/Oct/2026:03:51:12 +0000] "POST /vendor/phpunit/phpunit/Util/PHP/eval-stdin.php HTTP/1.1" 404 162 "-" "python-requests/2.31.0"
192.0.2.88 - - [18/Oct/2026:04:02:44 +0000] "POST /phpunit/phpunit/src/Util/PHP/eval-stdin.php HTTP/1.1" 404 162 "-" "Go-http-client/2.0"

On our own server, the 30 days to 18 October 2026

2,729

requests · about 91 a day

68

source addresses

Most requested variantsRequests
/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php94
/vendor/phpunit/phpunit/Util/PHP/eval-stdin.php76
/vendor/phpunit/src/Util/PHP/eval-stdin.php76
/vendor/phpunit/Util/PHP/eval-stdin.php76
/vendor/phpunit/phpunit/LICENSE/eval-stdin.php76
/vendor/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php76
/phpunit/phpunit/src/Util/PHP/eval-stdin.php76
/phpunit/phpunit/Util/PHP/eval-stdin.php76

LogTriage's production API server (Hetzner, Germany): port 22 open to the internet, key-only since 6 October 2026 (password and keyboard-interactive authentication off); nginx in front of the API on ports 80 and 443. Paths as requested, query strings dropped and identifiers redacted; counted weekly from the nginx access log. More in the monthly attack report.

The vulnerability it targets

CVE-2017-9841 CVSS 3.1 9.8 critical CISA KEV since February 15, 2022

PHPUnit · PHPUnit — PHPUnit Command Injection Vulnerability

Util/PHP/eval-stdin.php in PHPUnit before 4.8.28 and 5.x before 5.6.3 allows remote attackers to execute arbitrary PHP code via HTTP POST data beginning with a "<?php " substring, as demonstrated by an attack on a site with an exposed /vendor folder, i.e., external access to the /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php URI.

NVD description, quoted verbatim · published June 27, 2017

CISA required action: Apply updates per vendor instructions.

Sources: NVD and CISA's Known Exploited Vulnerabilities catalog, re-checked weekly.

What the request is

eval-stdin.php belongs to PHPUnit, the PHP testing framework, under its Util/PHP directory. PHPUnit is a development dependency: Composer installs it into the project’s vendor directory, and the file is only ever meant to be invoked by the test runner on a developer machine or a build agent, never over HTTP.

CVE-2017-9841 is what happens when it is reachable anyway. In the affected versions the script passes data it reads on standard input to the PHP interpreter, so a request body that starts with a PHP opening tag is executed by the web server as code — a code injection issue (CWE-94), with no authentication and no exploit chain in front of it. The NVD description above gives the affected version ranges and the canonical URI; everything in this page follows that text.

The many path variants in your logs exist because scanners do not know where your vendor directory lives. The canonical path is /vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php, but deployments ship it one level up (no src), one level down (a nested vendor/vendor), with the framework’s own prefix (/phpunit/phpunit/...), or under a sub-application directory. Each guess is one cheap request, and only one has to hit.

Who sends it

This is mass, untargeted scanning. The senders are internet-wide scanners sweeping address ranges, commodity botnets that chain this bug into cryptominer or webshell deployment because it is a one-request path to code execution, and research crawlers cataloguing exposed vendor folders. A request arriving at your edge says nothing about your organisation having been selected.

The user agent is weak evidence: some clients announce an HTTP library such as python-requests or Go-http-client, many copy a current Chrome string. The shape is more telling — a burst from one address within seconds, no Referer, no requests for the site’s own CSS, JavaScript or images, and a quick move on to other well-known files such as /.env or /.git/config once the PHPUnit paths return 404. If you see that pattern, the same address is worth checking against your /.env probe findings.

Is your server exposed?

Three things have to be true at once: the host serves PHP, a PHPUnit copy sits inside a document root, and its version falls in the affected range. Check what you already answered, then check live:

# Every eval-stdin path in the log, with the status code it got
awk 'tolower($7) ~ /eval-stdin\.php/ {print $9, $7}' /var/log/nginx/access.log \
  | sort | uniq -c | sort -rn | head -20

# Anything that was NOT 404/403 — this is the list that matters
awk 'tolower($7) ~ /eval-stdin\.php/ && $9 !~ /^(404|403)$/' /var/log/nginx/access.log

# Ask your own server, from your own server, and read the status code
curl -s -o /dev/null -w '%{http_code}\n' \
  http://127.0.0.1/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php

# Does a PHPUnit copy sit under a document root at all?
find /var/www /srv -path '*phpunit*' -name 'eval-stdin.php' 2>/dev/null

# Which version is installed, if any
grep -r -m1 '"phpunit/phpunit"' -A3 /var/www/*/composer.lock 2>/dev/null

Run the curl check against every vhost you serve, not just the default — a vendor directory left reachable on a staging or legacy host is the usual way this survives. Compare any installed version against the ranges in the NVD text above, and repeat the awk passes over rotated and compressed logs so you can tell whether a 200 was served before the file was removed.

How to block it

The durable fixes are deployment-level: point root at the application’s public directory so vendor has no URL at all, and install production dependencies without the dev requirements so PHPUnit is never deployed. Add an edge rule as a second layer, in every server block:

# Nothing under the Composer vendor tree should be served
location ^~ /vendor/ {
    access_log off;
    log_not_found off;
    return 404;
}

# Catch the file wherever a deployment hid it
location ~* /eval-stdin\.php$ {
    access_log off;
    return 404;
}

Returning 404 rather than 403 tells the scanner nothing about which paths exist. If an application genuinely publishes static assets from a vendor sub-path, narrow the first rule to the PHP files instead of the whole tree rather than dropping it, and verify with curl afterwards that the PHPUnit paths still answer 404.

Probes that arrive with it

Detect it

MITRE ATT&CK: T1595.002T1190

Frequently Asked Questions

Every one of these returned 404. Is there an incident here?
No. A 404 means the file is not there and nothing was executed, and this traffic is undirected background scanning that will not stop if you block the senders. The useful work is confirming no PHPUnit copy sits under a document root on any vhost, including staging and admin hosts, and alerting on non-404 answers to these paths rather than on the requests themselves.
The scanner used GET, not POST. Does that still count as an exploit attempt?
Treat it as reconnaissance. NVD describes the code execution as happening via HTTP POST data that begins with a PHP opening tag, so a bare GET mainly tells the scanner whether the file exists and what status code it gets back. Hosts that answer anything other than 404 or 403 are usually revisited with the POST that carries the payload.
Why would PHPUnit ever be on a production web server?
PHPUnit is a test framework normally installed as a Composer development dependency into the project's vendor directory. It reaches production when a deploy runs a plain composer install that includes dev requirements, when the whole working tree including vendor is rsynced or committed, and it becomes reachable when the web root points at the project directory instead of the application's public directory.
One of these paths returned 200 on our host. What is the clean-up?
Treat it as a potential remote code execution and work the host, not the URL. Pull the full request and response record for that address, look for follow-up requests to files you do not recognise under the web root, review PHP-FPM and web server error logs around that timestamp, check for new or modified files and scheduled tasks, and rotate any credentials the application process could read.

See what else is probing your server

Drop an nginx or Apache access log into the free browser analyzer: secret-file sweeps, exploit probes, scanners and credential stuffing are grouped by source with the evidence lines, and every known probe links back here. Nothing is uploaded.