A reachable phpinfo() page in production is not a direct code execution vulnerability, which is why teams often deprioritize it. But the output is a reconnaissance tool handed to an attacker for free: PHP version, every loaded extension, absolute file paths, environment variables (which frequently contain database passwords, API keys, and S3 credentials in containerized deployments), database connection settings, and internal network topology.

Automated scanners probe for info.php, phpinfo.php, test.php, debug.php, and similar filenames continuously. If any return a 200 with the full PHP configuration page, the attacker has a detailed map of your runtime. From there, version-specific CVEs, extension-specific exploits, and credential reuse attacks become targeted rather than speculative.

What phpinfo() exposes

The phpinfo() function outputs a single HTML page containing the complete runtime configuration of the PHP interpreter. In production, this page reveals several categories of sensitive information.

PHP version and build information. The exact PHP version, build date, server API, configure command, and operating system. This lets an attacker match your version against known CVEs and skip reconnaissance.

Loaded extensions and their versions. Every loaded PHP extension is listed, often with its version. If you are running a vulnerable version of an image processing, XML parsing, or database driver extension, the attacker knows exactly which exploit chain to attempt.

Absolute file paths. The document root, include paths, temporary directories, and extension directories are all displayed. This reveals your directory structure, which aids path traversal attacks, local file inclusion exploits, and webshell placement.

Environment variables. Every environment variable visible to the PHP process is dumped in the Environment section. In containerized deployments, this is frequently where database passwords, API keys, S3 access keys, mail server credentials, and other secrets live. A reachable phpinfo() in a container that passes secrets as env vars is effectively a credential dump.

PHP configuration directives. The page shows disable_functions, open_basedir, memory_limit, upload_max_filesize, opcache settings, and session configuration. An attacker can see which functions are disabled, whether open_basedir is enforced, and what the resource limits are. This tells them what attack techniques remain viable.

HTTP headers and server software. The server software and version, along with request headers, reveal the web server stack and sometimes internal IP addresses.

Database and service configuration. Connection settings, hostnames, and sometimes credentials that applications expose through their configuration are visible if the application bootstraps database connections before phpinfo() renders.

The severity depends on context. A standalone phpinfo() page on a server with no secrets in environment variables is a Medium information disclosure. A phpinfo() page in a containerized deployment that passes database passwords and S3 keys as environment variables is effectively Critical, because the page becomes a direct credential exposure that requires immediate rotation.

How to detect exposed phpinfo() endpoints

The same filenames that automated scanners probe are the ones you should check.

Probe common filenames

# Check common diagnostic filenames from localhost
for f in info.php phpinfo.php test.php debug.php php_info.php p.php; do
  code=$(curl -s -o /dev/null -w "%{http_code}" "http://localhost/$f")
  [ "$code" = "200" ] && echo "EXPOSED: /$f (HTTP $code)"
done
# Check from an external perspective (what an attacker sees)
for f in info.php phpinfo.php test.php debug.php php_info.php; do
  code=$(curl -s -o /dev/null -w "%{http_code}" "https://your-public-domain/$f")
  [ "$code" = "200" ] && echo "EXPOSED: /$f (HTTP $code)"
done

A 200 response means the page is reachable. A 404 means the file does not exist. A 403 means the file exists but the web server is blocking it (acceptable). Any 200 from an external IP is a finding that needs immediate attention.

Search the document root

# Find files that call phpinfo() in the web root
grep -rl "phpinfo()" /var/www/html --include="*.php"

This catches files that may not follow the common naming pattern but still call phpinfo(). Frameworks, vendor packages, and diagnostic bundles can include such files without obvious filenames.

Check access logs for active reconnaissance

# See if scanners have already found your phpinfo page
grep -E "(info|phpinfo|test|debug)\.php" /var/log/nginx/access.log | grep ' 200 '

If you see 200 responses for these filenames from unknown source IPs, the page has been accessed. Cross-reference those source IPs against known scanner ranges and your own monitoring.

How to remove and block phpinfo()

There are three layers of defense. Apply all of them. Relying on a single layer is how pages reappear after the next deploy or a framework update.

flowchart TD
    A["Reachable phpinfo detected"] --> B["Assess what leaked"]
    B --> C{"Secrets in env vars?"}
    C -- Yes --> D["Rotate credentials"]
    C -- No --> E["Standard remediation"]
    D --> E
    E --> F["Layer 1: delete files"]
    F --> G["Layer 2: disable_functions"]
    G --> H["Layer 3: web server block"]

Layer 1: delete the files

If a developer left info.php or phpinfo.php in the document root, delete it.

# Remove common diagnostic files
# WARNING: confirm these are the correct paths for your deployment before running
rm -f /var/www/html/info.php /var/www/html/phpinfo.php /var/www/html/test.php

This is the minimum. But files can come back: a deploy script, a framework scaffold, or a third-party library can reintroduce them.

Layer 2: disable the function at the engine level

The disable_functions directive in php.ini prevents phpinfo() from executing regardless of whether a file calling it exists. This is a PHP_INI_SYSTEM directive: it can only be set in php.ini and cannot be overridden per-request or via .htaccess.

; In php.ini
disable_functions = phpinfo, exec, passthru, shell_exec, system, show_source, highlight_file

The OWASP PHP Configuration Cheat Sheet recommends disabling phpinfo alongside other dangerous functions like exec, system, shell_exec, passthru, show_source, and highlight_file.

On PHP 8.0 and later, disabled functions behave as if they do not exist. function_exists('phpinfo') returns false, and it becomes possible to redeclare (polyfill) a disabled function. This is a behavioral change from PHP 7.x.

After changing disable_functions, reload PHP-FPM. The graceful reload (SIGUSR2) drains workers before the master re-execs, so there is a brief window with no workers. Plan for it.

# Graceful reload (SIGUSR2) -- brief service gap during worker drain
systemctl reload php8.3-fpm

Adjust the service name to match your distribution (php-fpm, php8.2-fpm, etc.).

Layer 3: block at the web server

Even if the file exists and the function is not disabled, the web server can block access to known diagnostic filenames.

nginx:

# Block common info-disclosure filenames
location ~* /(info|phpinfo|test|debug)\.php$ {
    deny all;
}

Ensure this location block precedes your generic PHP handler (location ~ \.php$) in the nginx config. nginx evaluates regex locations in order of appearance, so a later block never matches.

Apache (2.4+):

<FilesMatch "^(info|phpinfo|test|debug)\.php$">
    Require all denied
</FilesMatch>

If a file slips through in a deploy, the web server still blocks it.

Common pitfalls

Setting expose_php = Off does not block phpinfo(). The expose_php directive controls whether PHP advertises its version in the X-Powered-By HTTP response header and the legacy logo GUID easter egg. It does not control the output of phpinfo(). Setting expose_php = Off hides the version header but does nothing to protect a reachable info.php page. In older PHP versions, expose_php = Off suppressed the PHP logo and credits in phpinfo() output, which contributes to the misconception that it disables the page. That behavior no longer applies in any supported PHP version. The only reliable protections are deleting the file, disabling the function via disable_functions, or blocking access at the web server.

Environment variables in containers are the real risk. In containerized PHP-FPM deployments, secrets are frequently passed as environment variables (DB_PASSWORD, API_KEY, S3_SECRET_ACCESS_KEY). The phpinfo() output includes a full dump of the environment section. A reachable phpinfo() in a container that passes secrets as env vars is a direct credential exposure, not just an information leak. If you find a reachable phpinfo() in a containerized environment, rotate all credentials that were visible as environment variables before you remove the page.

Third-party libraries can ship phpinfo endpoints. Some applications and frameworks include diagnostic routes or files that call phpinfo(). A reachable phpinfo() may not be a file you created. Check application-level routes and vendor files, not just the document root. A framework upgrade can introduce a new diagnostic endpoint without an obvious filename.

The status page is a related exposure. The PHP-FPM status page (pm.status_path, typically /fpm-status or /status) and ping endpoint (ping.path, typically /ping) leak operational details: worker counts, request URIs, script paths, and pool configuration. In full mode, the status page exposes the exact PHP script paths and query strings of every active request. If you are auditing for phpinfo() exposure, audit the status page at the same time. The detection and blocking approach is identical: check if it returns 200 from an external request, then restrict access to localhost or internal IPs only.

# Check if the FPM status page is externally reachable
curl -s -o /dev/null -w "%{http_code}" "https://your-public-domain/fpm-status"
# A 200 means it is exposed -- restrict it

Signals to monitor

SignalWhy it mattersWarning sign
HTTP 200 on /info.php, /phpinfo.php, or similarConfirms a reachable phpinfo pageAny 200 response to these paths from an external IP
200 responses on .php files in upload or temp directoriesMay indicate webshell upload, not just info disclosureRequests to /uploads/*.php returning 200
disable_functions configuration driftEnsures phpinfo remains disabled at the engine levelphp -r 'echo ini_get("disable_functions");' no longer lists phpinfo
Status page returning 200 externallyRelated info-disclosure endpoint exposing request URIs and script pathscurl -I https://domain/fpm-status returns 200
Access log entries for diagnostic filenamesDetects active reconnaissance by automated scannersSustained probes for info.php, test.php, debug.php from rotating IPs

How Netdata helps

  • Web server log monitoring surfaces 200 responses on diagnostic filenames as they happen, making active probing against info.php and phpinfo.php visible before a human would find it in raw logs.
  • PHP-FPM collector metrics (active processes, listen queue, slow requests) provide context when a phpinfo page is hammered by scanners, which can consume worker slots and degrade legitimate traffic.
  • Per-second granularity captures transient probes that a 10-second or 30-second poll interval would miss, which matters because automated scanners move quickly through target lists.