— Artikel — № 052

052 —PHP

phpinfo() lezen: een topografische kaart van je host

Zet een phpinfo.php in de docroot en de muur aan beige tabellen die je terugkrijgt is een topografische kaart van de host. Lees hem sectie voor sectie.

Bovenaanzicht van geprint phpinfo-vel met inktcontouren, koperen HOST-plaat en rode lakzegel op botkleurig linnen.
Hero · gestileerd stilleven№ 052

Een klant stuurt SFTP-gegevens door met een vage notitie: "het contactformulier verstuurt sinds dinsdag niets meer, kun je even kijken." Je logt in, ziet een docroot die je nog nooit hebt gezien, en het eerste wat je doet voordat je iets aanraakt is één bestand van één regel in de webroot zetten:

<?php phpinfo();

Open hem in een browser. Wat je terugkrijgt is een muur aan beige tabellen waar de meeste mensen langs scrollen op zoek naar één specifieke waarde. Lees hem goed en de phpinfo()-output van een verouderde site is een topografische kaart van de host: PHP-versie, kernel, exact welke php.ini er draait, elke geladen extensie, wat de webserver denkt te zijn, en welke environment variables je code daadwerkelijk ziet.

Dit artikel loopt de pagina van boven naar beneden door, sectie voor sectie, met wat elk blok je vertelt.

Het banner-blok

De banner bovenaan geeft je vier feiten die het grootste deel van je volgende uur bepalen.

  • PHP-versie. PHP Version 7.4.33 betekent dat de host bevroren is sinds end-of-life in november 2022. WordPress core start nog wel op, maar Composer-installaties weigeren moderne constraints en de Stripe SDK 13+ laadt niet meer.
  • Systeemregel. Linux web17 5.10.0-26-amd64 #1 SMP Debian x86_64. Je weet nu de kernel, de hostnaam (vaak een verklapper van welk shared-hosting pakket je hebt), en de architectuur.
  • Build date. Vertelt je of het distropakket vers is of al twee jaar muf staat.
  • Server API. FPM/FastCGI versus Apache 2.0 Handler versus cli bepaalt welk config-bestand überhaupt relevant is. Een site achter nginx zegt FPM. Een cPanel shared host zegt meestal Apache 2.0 Handler. Zie je cli, dan kijk je naar de verkeerde binary.

Lees de Server API als eerste. De rest van de pagina leest anders bij FPM dan bij mod_php, omdat de ini-scan-map op een andere plek staat en een reload iets anders betekent.

Configuratiebestand en scan-map

Twee rijen die vaker verkeerd gelezen worden dan welke ook.

  • Configuration File (php.ini) Path: waar PHP een ini zoekt.
  • Loaded Configuration File: degene die daadwerkelijk gebruikt wordt.
  • Scan this dir for additional .ini files: meestal /etc/php/8.2/fpm/conf.d/.
  • Additional .ini files parsed: de daadwerkelijke lijst, in laadvolgorde.

Als Loaded Configuration File (none) zegt, draai je op compile-time defaults, wat op een shared host zelden voorkomt maar het gebeurt. Erger: als Path en Loaded niet overeenkomen, heeft iemand de ini at runtime overschreven. Check op een PHPRC environment variable of een php_admin_value directive in de vhost.

De scan-map is belangrijk omdat de meeste moderne distributies ini-snippets opsplitsen in losse bestanden per extensie. Op Debian en Ubuntu zie je 20-mysqli.ini, 20-opcache.ini, 30-xdebug.ini. Het numerieke prefix bepaalt de laadvolgorde. Wil je memory_limit netjes overschrijven, zet dan een bestand met een hoger prefix in de scan-map in plaats van de hoofd-ini aan te passen.

Geladen modules als vingerafdruk

Onder de Core-sectie toont phpinfo elke geladen extensie in een eigen tabel. Dit is de echte vingerafdruk van de host, en de plek waar de meeste "waarom werkt deze plugin niet"-tickets beantwoord worden.

Loop deze af, in volgorde:

  • opcache. Ontbreekt deze op een productiehost, vraag dan waarom. Inschakelen op een matig drukke WordPress-site halveert het CPU-gebruik grofweg.
  • mysqli en pdo_mysql. WordPress en de meeste CMS'en gebruiken standaard mysqli. Oudere code die nog rechtstreeks mysql_* aanroept is dood sinds PHP 7.0. Kom je dat tegen, dan is dat je bug.
  • gd versus imagick. De WordPress wp_image_editor pakt stilzwijgend wat beschikbaar is. Ontbreken beide, dan blijft elke geüploade afbeelding op het originele formaat staan.
  • curl. Geen curl betekent geen uitgaand HTTPS voor Wordfence, Akismet, of welke API-koppeling dan ook.
  • intl. WooCommerce 8+ start niet zonder.
  • redis of memcached. Aanwezigheid zegt dat de host een object cache verwacht. Afwezigheid betekent dat de Redis-backend van W3 Total Cache installeren zinloos is.
  • xdebug. Zie je deze op een productiehost, haal hem eraf. Hij kost meetbare wall-clock tijd, ook als je niet actief aan het debuggen bent.

Elke extensietabel toont ook de gecompileerde versie. Mismatches tussen wat je hier ziet en wat composer.json verwacht zijn de oorzaak van de helft van de "werkt op staging, breekt op productie"-tickets.

Het env- en server-variabelenblok

Onderaan staan drie tabellen die er bijna identiek uitzien en waar mensen over struikelen.

  • Apache Environment (of Environment bij FPM): variabelen die de webserver aan PHP doorgeeft.
  • HTTP Headers Information: hoe de huidige request eruit zag.
  • PHP Variables: de $_SERVER, $_ENV en $_REQUEST arrays zoals PHP ze op dit moment ziet.

De interessante is meestal Environment. Op een shared host, kijk naar DOCUMENT_ROOT om te bevestigen in welke map je daadwerkelijk zit (gesymlinkte vhosts zijn standaard). Kijk naar HTTP_X_FORWARDED_PROTO als de site achter een load balancer hangt. Staat deze op https maar denkt PHP nog steeds dat de request HTTP is, dan moet je wp-config.php patchen:

if (isset($_SERVER['HTTP_X_FORWARDED_PROTO'])
    && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
    $_SERVER['HTTPS'] = 'on';
}

Kijk naar PATH om te zien welke shell-binaries exec()-aanroepen vinden. Hosts die PATH dichttimmeren tot /usr/bin:/bin breken plugins die uitwijken naar wkhtmltopdf in /usr/local/bin.

De rij disable_functions

Scroll in het Core-blok naar disable_functions. Op een hard dichtgezette shared host leest deze rij als een grafsteen:

exec, passthru, shell_exec, system, proc_open, popen,
curl_multi_exec, parse_ini_file, show_source, symlink

Elk van die functies wordt ergens door een legitieme plugin gebruikt. Faalt een contactformulier stilletjes op een nieuwe host, dan is dit de eerste plek om te kijken. Het omgekeerde geldt ook: is deze rij leeg op een productiehost, behandel dat als een bevinding, niet als een feature. De OWASP PHP configuration cheat sheet raadt aan minstens de vier shell-spawnende functies uit te schakelen.

Session, OPcache en upload-limieten

Vier rijen onderaan die de meeste "het brak toen het bestand groot werd"-incidenten verklaren.

  • upload_max_filesize en post_max_size. De tweede moet groter zijn dan de eerste, en beide groter dan nginx's client_max_body_size. Faalt een upload van 12 MB stilletjes, dan is een van deze drie de oorzaak. Zie de php.net ini reference voor de interactie.
  • memory_limit. 256M is de moderne ondergrens voor WordPress. Alles lager en de block editor geeft een wit scherm bij lange berichten.
  • max_execution_time. 30s is de default. Langlopende imports vragen hier een hogere waarde in een aparte FPM pool, niet globaal.
  • session.save_path. Staat deze op /tmp op een shared host, dan worden sessies door de cron van iedere tenant opgeruimd. Hosts die sessies serieus nemen geven je een pad per gebruiker.

Voor OPcache, check opcache.enable=1 en opcache.validate_timestamps. Met timestamps uit hebben bestandswijzigingen pas effect na een reload van de pool. De juiste instelling voor productie, de verkeerde instelling voor "waarom verschijnt mijn CSS-wijziging niet".

Wat we hier omheen bouwden

Toen we Pier bouwden liepen we steeds tegen hetzelfde patroon aan: tien minuten kwijt aan het opnieuw uitvogelen van diezelfde handvol phpinfo-feiten iedere keer dat we een klantsite openden. Wat we er uiteindelijk mee gedaan hebben is een permanente docked view die PHP-versie, ini-pad, geladen modules en disable_functions naast de bestandsboom toont, ververst bij elke nieuwe verbinding.

Het kleinste wat je vandaag kunt doen: zet een phpinfo.php in een staging site waar je toch al aan werkt, lees hem één keer van boven naar beneden met dit artikel ernaast, en verwijder daarna het bestand. De volgende keer dat een host je verrast, weet je naar welke sectie je moet scrollen.

— Vragen —

Is het veilig om phpinfo() op een productieserver te laten staan?

Nee. De pagina toont de PHP-versie, het OS, elke extensie en je docroot-pad, allemaal informatie die aanvallers sneller maakt. Verwijder het bestand zodra je het gelezen hebt.

Waar pas ik php.ini aan op shared hosting?

Meestal kan dat niet rechtstreeks. Kijk in phpinfo bij de scan-map-rij. De meeste shared hosts laten je een user.ini of een .htaccess php_value override neerzetten; cPanel heeft een MultiPHP INI Editor.

Waarom staan mysqli en pdo_mysql allebei in de lijst?

Het zijn twee aparte extensies om dezelfde MySQL-clientlibrary heen. WordPress gebruikt mysqli; de meeste moderne frameworks gebruiken PDO. Beide geladen hebben is normaal en kost niets noemenswaardigs.