/phpinfo.php Web access log Triage: medium

GET /phpinfo.php and /info.php Requests: Scanners Mapping Your PHP Setup

Published

An automated hunt for a page that calls PHP's phpinfo() function, which prints the server's PHP version, build and module list, configuration paths and environment variables to anyone who loads it.

What it looks like

203.0.113.7 - - [18/Oct/2026:03:51:11 +0000] "GET /phpinfo.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] "GET /info.php HTTP/1.1" 404 162 "-" "python-requests/2.31.0"
192.0.2.88 - - [18/Oct/2026:04:02:44 +0000] "GET /_profiler/phpinfo HTTP/1.1" 404 162 "-" "Go-http-client/2.0"

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

570

requests · about 19 a day

46

source addresses

Most requested variantsRequests
/phpinfo.php79
/info.php65
/_profiler/phpinfo49
/test.php46
/i.php11
/admin/phpinfo.php9
/old/phpinfo.php9
/site/phpinfo.php9

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.

What the request is

phpinfo() is a PHP function that prints a full report on the running interpreter. A page that calls it returns the PHP version and build date, the operating system and build system, the loaded php.ini and any additional .ini files, the compile options, every loaded extension with its own settings and master/local values, the environment variables of the PHP process, the $_SERVER variables of the current request including DOCUMENT_ROOT and SERVER_ADDR, and the HTTP headers of the request that fetched it.

None of that is an exploit on its own, and none of it is secret by design — it is a diagnostic page. The problem is that it is usually created in a hurry (info.php, test.php, i.php dropped in the web root to prove a new install works) and then left behind, where it answers anyone. For an attacker it turns guesswork into a shopping list: exact version strings for the interpreter and every extension, whether open_basedir and disable_functions are set, where sessions are stored, and which paths exist on disk. Environment variables are the part that stings most often, because container and PHP-FPM deployments frequently pass database passwords and API keys to the process that way — the same class of secret that a reachable .env file hands over.

/_profiler/phpinfo is a different route to the same output: the phpinfo panel of Symfony’s web profiler, which belongs to the development environment and should not be reachable on a production deployment at all. A 200 there means the profiler itself is exposed, which is a larger problem than the phpinfo output alone.

Who sends it

This is undirected, internet-wide scanning. The senders are mass scanners sweeping address ranges, botnets that fingerprint hosts before trying version-specific exploits, and research crawlers that catalogue exposures. A request to /phpinfo.php does not mean your organisation was selected for anything.

User agents are not evidence of identity. Some clients announce an HTTP library such as python-requests or Go-http-client; many copy a current Chrome string. The shape of the traffic is more telling: several paths from one address within a few seconds, no Referer, no requests for the stylesheets or images a browser would pull, and an immediate move on to other well-known filenames once each one returns 404.

Is your server exposed?

Check what you already answered, then ask your own host:

# phpinfo-style paths in the log, with the status code each one received
awk '$7 ~ /(phpinfo|php_info|_profiler|\/info\.php|\/test\.php|\/i\.php)/ {print $9, $7}' \
  /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20

# Anything that was NOT a 404 or 403 — this is the list that matters
awk '$7 ~ /(phpinfo|php_info|_profiler|\/info\.php|\/test\.php|\/i\.php)/ && $9 !~ /^(404|403)$/' \
  /var/log/nginx/access.log

# Ask your own server, from your own server, one vhost at a time
for p in /phpinfo.php /info.php /test.php /i.php /_profiler/phpinfo; do
  printf '%s -> ' "$p"
  curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: www.example.com' "http://127.0.0.1$p"
done

# Leftover diagnostic files anywhere under a document root
grep -rl --include='*.php' 'phpinfo' /var/www /srv 2>/dev/null | head

Run the curl loop for every Host you serve, including staging and admin vhosts — a rule added to the main site and forgotten elsewhere is the usual reason one of these stays live. Rotated logs deserve the same awk pass, since an exposure may already have been closed after the fact.

How to block it

Deleting the file is the real fix; the server block is the safety net. Return 404 rather than 403 so a scanner learns nothing about which names exist:

# Diagnostic PHP pages: never served, never logged
location ~* ^/(?:phpinfo|php_info|info|test|i)\.php$ {
    access_log off;
    log_not_found off;
    return 404;
}

# Symfony web profiler has no place on a production host
location ^~ /_profiler/ {
    access_log off;
    return 404;
}

Check first that no real application page uses one of those names — info.php and test.php are generic enough that a legacy app may genuinely serve one, in which case narrow the regex to the names you do not use.

Two php.ini settings harden what is left. Adding phpinfo to disable_functions stops the function from producing output even if a file calling it reappears, at the cost of breaking diagnostic tooling that relies on it. Setting expose_php = Off removes the PHP version from the X-Powered-By header, so the version is not handed out by every ordinary response either. See the PHP manual entry for phpinfo() for the full list of sections the function prints.

Probes that arrive with it

Detect it

MITRE ATT&CK: T1595.003T1592.002

Frequently Asked Questions

Our server answers all of these with 404. Is there anything to do?
Not as an incident — a 404 discloses nothing and this traffic is constant background scanning that will not stop. The useful work is preventive: confirm no diagnostic PHP file is left in any document root, confirm the Symfony profiler is off outside development, and alert on a non-404 status for these paths rather than on the requests themselves.
A phpinfo page of ours really was reachable. How bad is that?
It is information disclosure, not code execution, but it is a strong head start for an attacker. Treat the PHP version, extension list and versions, configuration file paths, document root, server address and every environment variable shown as known to whoever loaded it, and rotate any secret that your deployment passes to PHP-FPM or the web server as an environment variable.
Why do scanners ask for /test.php and /i.php as well?
Those are the filenames administrators invent when checking that a new PHP install works, and the files are routinely forgotten afterwards. The scanner has no idea what you named yours, so it walks a short list of likely names and likely subdirectories; each attempt is one cheap request and only one has to return 200.
Should I block the scanning IP addresses?
Rarely worth it. The source pool rotates constantly and the traffic is undirected, so a blocklist ages out faster than you can maintain it. Remove the files, return 404 cheaply at the edge, and spend the effort on an alert for any non-404 response to a phpinfo-style path across every vhost you serve.

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.