Usage

Install, point it at a host, a directory, an image or a single file, and choose the report format.

Install #

PHP 7.4 or newer, core extensions only.

composer global require jorgsowa/php-ini-audit
# or, from a checkout, without installing:
bin/php-ini-audit --help
# or as one file to copy onto a server:
php -d phar.readonly=0 scripts/build_phar.php
php build/php-ini-audit.phar --help

Choosing what to audit #

CommandWhat is read
php-ini-auditEvery installation found in the known locations on this host.
php-ini-audit /etc/php/8.3/fpm/php.iniThat php.ini plus the sibling conf.d/ or php.d/ and FPM pools.
php-ini-audit ./backup/Walks the directory: every php.ini, loose *.ini, pool *.conf, .user.ini and .htaccess.
php-ini-audit --root /mnt/rootfsHost discovery inside an extracted image, chroot or mounted disk. Symlinks stay inside the root.
php-ini-audit --webroot /var/www/htmlAdds .user.ini/.htaccess below the document root as overlays, and marks paths inside it as web-served.
php-ini-audit --ini app.ini --scan-dir conf.d --pool www.confAn explicit installation, like php -c app.ini with PHP_INI_SCAN_DIR=conf.d.
php -i | php-ini-audit --phpinfo -The effective values a running PHP reports: saved phpinfo() HTML, the copied page or php -i output. Also available in the browser, together with pasted php.ini, pool and .htaccess files.
php-ini-audit discoverLists the files that would be read, without auditing.

Exporting a container image for --root needs no running container:

mkdir rootfs
docker export "$(docker create php:8.3-fpm)" | tar -x -C rootfs
php-ini-audit --root rootfs

Scan options #

OptionDescription
--profile production|developmentDefault production. Development lowers disclosure findings (e.g. display_errors becomes info) and skips production-only rules.
--sapi NAMEcli, fpm, apache2, cgi, litespeed, embed or unknown. Normally inferred from the path; unknown is treated as web-facing.
--php-version X.Y[.Z]Exact version for known-vulnerability checks. Otherwise it is read from php_version.h or package databases on host and --root scans, or the branch is inferred from the path (/etc/php/8.2/, php82, ea-php81).
--path-checks auto|on|offInspect permissions of paths named in the config. auto enables it for host and --root scans, not for copied files whose paths would point at the wrong machine.
--min-severity LEVELHide findings below critical, high, medium, low or info. Default low for text, which counts hidden info findings in the summary, and info for the other formats.
--only ID|CATEGORYRun only these rules; repeatable (--only SESS --only EXEC-001).
--skip ID|CATEGORYSkip rules; repeatable.
--format text|json|sarif|markdownReport format; text is colored on a terminal unless --no-color or NO_COLOR is set.
--group-by target|ruleText only. target (default) lists findings per configuration; rule lists each rule once with the configurations it affects, which reads better on hosts with several PHP versions.
--summaryText only. Print one line of severity counts instead of the report.
-o, --output FILEWrite the report to a file.
--fail-on LEVELExit with status 1 when any finding is at or above LEVEL.
--max-depth NDirectory walk depth, default 12.

Reading the output #

Each finding has:

Findings on an overlay only cover what the overlay changes, so php.ini problems are not repeated for every pool.

Formats #

JSON

{
  "tool": {"name": "php-ini-audit", "version": "0.1.0"},
  "profile": "production",
  "summary": {"by_severity": {"critical": 4, "high": 4, …}, "by_category": {…}, "by_target": {…}},
  "targets": [{"name": "/etc/php/8.2/fpm/php.ini", "sapi": "fpm", "php_version": "8.2", "files": […]}],
  "findings": [{
    "rule_id": "EXEC-001", "severity": "critical", "confidence": "high",
    "message": "allow_url_include is enabled", "directive": "allow_url_include", "value": "1",
    "file": "/etc/php/8.2/fpm/php.ini", "line": 22, "from_default": false,
    "target": "/etc/php/8.2/fpm/php.ini", "cwe": "CWE-98", "recommendation": "…"
  }]
}

SARIF

SARIF 2.1.0 with security-severity properties. GitHub code scanning reads it directly. Critical and high map to error, medium to warning, and low and info to note.

Markdown

Severity summary plus one table per severity, ready for a pull-request comment or ticket.

Exit codes #

CodeMeaning
0Audit completed. Findings do not change the exit code unless --fail-on is given.
1A finding at or above the --fail-on threshold exists.
2Usage error: unknown rule or category, invalid --root, an explicitly passed path that cannot be read, no PHP configuration audited, or a report that cannot be written with -o.

CI: GitHub code scanning #

name: php-config-audit
on: [push, pull_request]
permissions:
  contents: read
  security-events: write
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with: {php-version: "8.4"}
      - run: composer global require jorgsowa/php-ini-audit
      - run: ~/.composer/vendor/bin/php-ini-audit docker/php/ --format sarif -o php-ini.sarif --fail-on high
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with: {sarif_file: php-ini.sarif}

Rule catalogue from the CLI #

php-ini-audit rules                     # id, severity, category, title
php-ini-audit rules SESS-001            # full explanation of one rule
php-ini-audit rules --format json       # machine-readable catalogue