073 —Drupal
Drupal 7 EOL-triage: de audit-checklist van tien minuten
Je erft een Drupal 7-site om 23:41 op zondagavond. Maandag wil de klant een rebuild-offerte. Dit is wat je écht moet checken voor je reageert.
Een klant mailt zondagavond. De factuurpagina van hun boekhouder doet het niet, de IT'er die de site bouwde verhuisde in 2019 naar Berlijn, en het contactformulier werkt nog, maar de Over-pagina geeft een fatal. De site draait op Drupal 7. Je hebt geen D7-installatie meer aangeraakt sinds de Drupalgeddon 2-patches uitkwamen, en maandagochtend moet je een rebuild-offerte versturen.
Voor je die offerte gaat scopen, ben je jezelf tien minuten triage verschuldigd. Drupal 7 ging op 5 januari 2025 voorbij de officiële end-of-life-datum, wat betekent dat het Drupal Security Team geen advisories meer publiceert voor core of voor contrib-modules. Wat je nu nog vindt, vind je zelf. Wat je mist, betaalt de klant. Dit is de audit-checklist die we bij elke Drupal 7-overdracht doorlopen, in volgorde.
Minuut één tot drie: wat de voordeur lekt
Drie commando's vertellen je meteen of de site stilletjes is onderhouden of stilletjes is weggerot. Je kunt ze vanaf je eigen machine draaien, geen credentials nodig.
curl -sI https://client-site.example/ | grep -i x-generator
curl -s https://client-site.example/CHANGELOG.txt | head -3
curl -sI https://client-site.example/user/login | head -1
De eerste regel geeft de X-Generator-header terug, die op een standaard Drupal 7-installatie Drupal 7 (https://www.drupal.org) leest. Staat hij aan, dan is je versie publiek. Is CHANGELOG.txt leesbaar vanaf de public document root, dan kan een aanvaller de exacte minor-versie aflezen uit de bovenste regel en opzoeken welke CVE's van toepassing zijn. Geeft /user/login een platte 200 zonder honeypot en zonder flood control, dan staat brute force open.
Drie dingen op je patch-lijst voor je iets anders doet:
- Blokkeer
CHANGELOG.txt,INSTALL*.txt,UPGRADE.txt,MAINTAINERS.txtenCOPYRIGHT.txtop .htaccess-niveau. - Strip
X-Generatorvia een custom module of op webserver-niveau. - Bevestig dat flood control daadwerkelijk afgaat in
admin/config/people/ip-blocking.
Het .htaccess-blok is één plakje:
<FilesMatch "(CHANGELOG|INSTALL|UPGRADE|MAINTAINERS|COPYRIGHT)\.txt$">
Require all denied
</FilesMatch>
Minuut drie tot zes: settings.php en het database-oppervlak
SSH of SFTP de webroot in. Open /sites/default/settings.php van boven naar beneden. Je zoekt vier specifieke dingen, en een zorgvuldige lezing kost ongeveer negentig seconden.
$databases['default']['default'] = array(
'database' => 'd7_main',
'username' => 'd7user',
'password' => 'hunter2', // 1. hergebruikt op staging? roteren.
'host' => 'localhost',
'driver' => 'mysql',
);
$update_free_access = FALSE; // 2. MOET FALSE zijn in productie
$drupal_hash_salt = '...'; // 3. bestaat en is lang
// 4. is $base_url hardcoded, en is hij https?
Nu je er toch bent, richt een SQL-client op de database en kijk direct in de users-tabel. De structuur van Drupal 7's users is sinds 2011 onveranderd, dus deze drie queries werken altijd.
SELECT COUNT(*) FROM users WHERE status = 1;
SELECT name, mail, login FROM users WHERE uid IN (1,2,3,4,5);
SELECT name, FROM_UNIXTIME(login) AS last_login FROM users
ORDER BY login DESC LIMIT 10;
uid 1 is de superuser. Heet die admin of root en was de laatste login in 2017, dan is dat account een opstapje, en moet je hem uitzetten voor de volgende back-up draait. De password-kolom slaat hashes op met prefix $S$, de portable phpass-implementatie van Drupal. Een oudere $P$-prefix bij een legacy import betekent dat de hash nooit is afgemigreerd en zwakker is dan de rest. De password hashing-docs op php.net zijn de juiste plek om een junior engineer naartoe te sturen die vraagt waarom dit uitmaakt.
Toevoegen aan de patch-lijst: roteer het database-wachtwoord, roteer $drupal_hash_salt, roteer het uid 1-wachtwoord, bevestig dat $update_free_access op FALSE staat, en audit iedereen met de administer site configuration-permissie.
Minuut zes tot tien: het bestandssysteem en de .htaccess
De twee meest opleverende mappen op elke Drupal 7-site zijn /sites/default/files en /sites/all/modules/contrib. De eerste vertelt je wat gebruikers de afgelopen tien jaar hebben geüpload. De tweede vertelt je welke contrib-modules patches nodig hebben die je niet meer gratis krijgt.
find sites/default/files -type f -name "*.php*" 2>/dev/null
find sites/default/files -type f -newer /tmp/marker | head -20
ls -la sites/all/modules/contrib/ | wc -l
Elk PHP-bestand onder /files is een rode vlag. De standaard per-directory .htaccess van Drupal 7 blokkeert PHP-uitvoering binnen de files-boom, maar als de webserver zonder AllowOverride draait, of als de map uit een gedeeltelijke back-up is teruggezet, is dat blok weg. De OWASP-gids over unrestricted file upload is dé referentie voor waarom dit de meest voorkomende opstap is bij verouderde CMS-installaties.
De .htaccess in de webroot van een schone D7-installatie bevat het canonieke rewrite-blok en een set php_value-regels. Diff wat in productie staat tegen de upstream uit de Drupal 7-tarball:
diff .htaccess ~/drupal-7.103/.htaccess
Eigen regels onder het canonieke commentaarblok zijn prima. Canonieke regels die boven dat blok zijn verwijderd, zijn dat niet.
Tenslotte: bevestig welke PHP-versie er daadwerkelijk draait. Drupal 7 wordt ondersteund tot en met PHP 8.1 met de D7-naar-D8-compatibility patches. Daarboven sta je er alleen voor. Draait de site op PHP 7.4 of ouder, dan telt de EOL-klok van de hostingmaatschappij zelf net zo zwaar als die van Drupal.
php -v
curl -s https://client-site.example/admin/reports/status | grep -i 'php version'
De offerte die je nu kunt schrijven
Na tien minuten weet je vier dingen die je om 23:41 niet wist. Welke contrib-modules kritiek verouderd zijn. Of de site zijn versie lekt naar iedereen met curl. Of het database-oppervlak hergebruikte credentials of vergeten superusers bevat. Of iemand een PHP-shell heeft geüpload naar /files. Dat is genoeg om een rebuild-offerte te schrijven met drie eerlijke regels: een directe hardening-pass op uurbasis, een bevroren mirror voor forensisch onderzoek, en de rebuild zelf.
Het patroon bij Drupal 7-overdrachten is consistent. De klant denkt dat hij voor een rebuild betaalt, en in week twee kom je erachter dat hij stilletjes ook voor een ongepatchte RCE betaalde. Triage eerst, zodat de scope van de rebuild de rebuild is, niet de schoonmaak met een rebuild eraan vast.
Toen we Pier bouwden, liepen we keer op keer tegen dezelfde frictie aan bij dit soort triage-rondes: tien minuten SSH plus phpMyAdmin plus de bestandsmanager van de hostingmaatschappij plus een los notitiebestand. Onze oplossing werd om de SFTP-browser, de MySQL editor en de version history in één paneel te zetten, zodat de audit en de patch in hetzelfde spoor leven.
Pak één verouderde site die je dit jaar erfde en draai de drie curl-commando's hierboven erop voor je vanavond je laptop dichtklapt. Wat ze ook teruggeven, je weet er meer over dan vanochtend.
— Vragen —
Is Drupal 7 nog veilig om te draaien na end-of-life?
Niet zonder een betaalde extended-support leverancier. Het Drupal Security Team is in januari 2025 gestopt met advisories, dus elke nieuwe core- of contrib-CVE patch je in je eentje.
Wat is de snelste manier om de X-Generator-header weg te halen?
Strip hem in een custom module via hook_page_attachments_alter, of op webserver-niveau met Header unset X-Generator in Apache, of more_clear_headers in nginx.
Patchen voor de rebuild-offerte, of eerst offreren?
Patch de overduidelijke gaten in hetzelfde uur dat je ze vindt. Een lekkende site die de klant geld blijft kosten gedurende een rebuild-traject van vier weken is een groter probleem dan een offerte die een dag later komt.