How it works

Four steps: find the files, parse them as PHP would, layer them into effective configurations, then evaluate every rule against each one.

Safety model #

1. Discovery #

Without arguments, these locations are searched (inside --root when given):

Platformphp.iniScan dir / pools
Debian, Ubuntu/etc/php/*/{cli,fpm,apache2,cgi}/php.iniconf.d/, pool.d/
RHEL, Fedora, Remi/etc/php.ini, /etc/opt/remi/php*/php.ini/etc/php.d/, /etc/php-fpm.d/
Alpine, SUSE, Arch/etc/php*/php.ini, /etc/php*/*/php.ini, /etc/php/php.iniconf.d/, php-fpm.d/
Official Docker images/usr/local/etc/php/php.iniconf.d/, /usr/local/etc/php-fpm.d/
FreeBSD/usr/local/etc/php.ini/usr/local/etc/php/
Homebrew/opt/homebrew/etc/php/*/php.ini, /usr/local/etc/php/*/php.iniconf.d/, php-fpm.d/
cPanel, Plesk, CloudLinux/opt/cpanel/ea-php*/root/etc/, /opt/plesk/php/*/etc/, /opt/alt/php*/etc/php.d/
BundlesXAMPP, MAMP, Bitnami, Zend Server, /usr/local/lib/php.inisibling conf.d/
WindowsC:\php, XAMPP, WAMP, Laragon, C:\Program Files\PHP\*—

The SAPI and PHP version come from the path, e.g. /etc/php/8.2/fpm/ is FPM on 8.2. A PHP directory that has php.ini-production or a conf.d/ but no php.ini is audited as running on built-in defaults (CFG-006). That is the layout of the official Docker images.

2. Parsing like PHP #

3. Layering #

A base configuration is built in PHP's order, with the last assignment winning:

  1. PHP's compiled-in defaults
  2. php.ini
  3. scan-directory *.ini files in alphabetical order

Each of these is then evaluated as a separate overlay on top of its base:

OverlayCan changeNotes
PHP-FPM poolanythingphp_admin_value[disable_functions] and disable_classes append to php.ini, as in PHP-FPM. Pool settings (listen, user, security.limit_extensions…) feed the FPM rules.
[PATH=…] / [HOST=…] sectionsanythingApplied only to matching CGI/FastCGI requests.
.user.iniPHP_INI_PERDIR, PHP_INI_ALLAttached to FPM/CGI bases.
.htaccessPHP_INI_PERDIR, PHP_INI_ALLAttached to Apache bases; php_admin_* is a configuration error there.

An overlay that tries to set a PHP_INI_SYSTEM directive, such as disable_functions or allow_url_fopen in .user.ini, does not take effect in PHP. The scanner reports it (CFG-005) rather than counting it as protection. Overlay findings only cover what the overlay changes, so base findings are not repeated.

Built-in defaults matter #

When a directive is not set anywhere, PHP uses its compiled-in default, not the value in php.ini-production. Several of those defaults are insecure:

DirectiveBuilt-in defaultRule
display_errors1ERR-001
register_argc_argv1EXEC-011
expose_php1ERR-004
enable_dl1EXEC-008
session.use_strict_mode0SESS-001
session.cookie_httponly0SESS-005
cgi.fix_pathinfo1CGI-002
disable_functions, open_basediremptyEXEC-005, FS-001

Findings caused by a default say at built-in default and have "from_default": true in JSON.

4. Rule evaluation #

Rules declare the SAPIs they apply to. Web-only checks such as session cookies or open_basedir are skipped for cli, and an unknown SAPI is treated as web-facing. The development profile caps disclosure rules at info and skips production-only rules.

Known vulnerabilities and the PHP version #

Some PHP vulnerabilities can only be exploited when a feature is enabled. CVE-2024-4577 needs php-cgi on Windows, CVE-2024-9026 needs catch_workers_output in an FPM pool, and the 2025 HTTP-wrapper bugs need allow_url_fopen. For each tracked CVE the scanner checks two things:

  1. Version: is the PHP patch release older than the fix for its branch? Branches that reached end of life between fixed branches never got the fix, so they count as affected.
  2. Configuration: does the effective configuration use the vulnerable feature?

A CVE is reported only when both are true. Fixed-in releases come from the official PHP 8 and PHP 7 changelogs. VULN-001 also reports branches past, or within 180 days of, the end of security support.

Every other publicly disclosed CVE fixed in a later release (parsers, extensions, bundled libraries) is listed in one warning per configuration, VULN-003, with the severity of the worst one. Fixed-in releases come from the changelogs and php-src security advisories, severities from those advisories or NVD.

Where the version comes from

The exact version is read from files, never by running php -v:

SourceExampleConfidence
--php-version X.Y.Zvalue you passhigh
PHP's php_version.h header/usr/local/include/php/main/php_version.h (official Docker images)high
Alpine package database/lib/apk/db/installedhigh
Homebrew links/opt/homebrew/opt/php@8.3 → Cellar/php@8.3/8.3.12high
dpkg database, deb.sury.org builds8.3.12-1+ubuntu22.04.1+deb.sury.org+1high
dpkg database, distribution builds8.2.7-1~deb12u1low: Debian and Ubuntu backport fixes without changing the upstream version
directory name/etc/php/8.2/branch only

When only the branch is known, the CVEs that apply to the configuration are summarised in one finding (VULN-002) with the release that fixes each, instead of one finding per CVE. Version files are read for host scans and --root scans; for copied configuration files, pass --php-version. RPM databases are binary and are not read.

Path checks #

About 25 directives name paths: extension_dir, session.save_path, upload_tmp_dir, error_log, mail.log, opcache.file_cache, openssl.cafile, curl.cainfo, sendmail_path, absolute extension= paths and others. Each gets a severity for two situations:

Configuration files themselves are checked as well (FS-011).

Limitations #