— Artikel — № 065

065 —Magento

Magento-audit: 40-minuten checklist voor overgenomen shop

Een bureau van 22 man neemt een Magento 2.3-shop over. De vorige dev is onbereikbaar. Dit is de audit van 40 minuten die we draaien voor je iets aanraakt.

Bovenaanzicht op linnen: auditchecklist, CLI-werkblad, ER-blauwdruk, messing plaatje, stopwatch, liniaal, lakzegel.
Hero · gestileerd stilleven№ 065

Een Nederlands bureau waarmee we samenwerken nam vorige maand een Magento 2.3.5-shop over. De vorige developer liet geen documentatie achter, stopte in april met mailen, en de klant gaf het FTP-wachtwoord door op een post-it. Hun eerste reflex was inloggen, rondklikken in de admin, en kijken wat er stond. Dat kostte ze twee uur en een corrupte catalogus-cache voordat ze één bruikbare vraag hadden beantwoord.

De 40-minuten audit hieronder draaien we voor we ook maar iets aanraken op een overgenomen Magento-shop. De shop wordt er niet door gerepareerd. Hij vertelt je wel of de shop het repareren waard is, waar de mijnen liggen, en wat je moet offreren. Elke overgenomen verouderde site die we zijn binnengelopen, faalde op één van de zes plekken die deze audit checkt.

Minuut 0 tot 5: vaststellen waar je eigenlijk naar kijkt

Voor de SSH, voor de admin, voor je de database opent: stel drie feiten vast.

  1. Magento-versie en editie
  2. PHP-versie
  3. Of Composer in gebruik is of dat de codebase een tarball is

Log in via SSH en draai:

php bin/magento --version
php -v
ls -la composer.json composer.lock 2>/dev/null

Je wilt output zoals Magento CLI 2.3.5-p2 en PHP 7.2.34. Zie je Magento 1.x of PHP onder 7.4, dan audit je geen shop, dan audit je een security-incident in wording. Magento 1 is sinds juni 2020 end-of-life. Magento 2.3 sinds september 2022.

Mist composer.json, dan zat de vorige dev met de hand in vendor/ en app/code/ te editen. Schrijf het op. Elke volgende stap wordt zwaarder.

Pak ook de deploy mode:

php bin/magento deploy:mode:show

De developer-mode in productie is een trage lek. De default-mode verbergt errors en maakt debuggen vervelend. Je wilt production, en als dat niet zo is, is dat de eerste zin in je overdrachtsrapport.

Minuut 5 tot 15: breng het maatwerk in kaart

Het echte risico bij Magento zit niet in de core. Het zit in de achttien third-party modules waarvan de auteurs in 2019 stil zijn gevallen.

php bin/magento module:status | head -100
composer show --direct 2>/dev/null

Verdeel de output in je hoofd in drie bakken. Core modules onder Magento_* kun je negeren, tenzij er één uitstaat. Bekende leveranciers (Amasty, Mageplaza, Mirasvit, Aheadworks) check je tegen hun site voor de laatste versie. Alles onder app/code/Vendor/Module is de bak waar je weekenden aan kwijt bent. Lees elke regel.

find app/code -maxdepth 2 -type d

Open voor elke custom module etc/module.xml en composer.json als die aanwezig is, en grep dan op de gevaarlijke patronen:

grep -r "eval(" app/code/ | grep -v "// "
grep -r "shell_exec\|passthru\|system(" app/code/
grep -r "base64_decode" app/code/ | head -20
grep -r "file_get_contents.*http" app/code/

Een hit op de eerste twee is een rode vlag. Hits op de laatste twee zijn meestal prima, maar wel het lezen waard in context. Een vorige developer leverde ooit op een shop die we overnamen een valutaconversie-module uit die elke cron-tick koersen ophaalde van een hardcoded IP-adres. Het IP bleek een huisaansluiting in Wit-Rusland.

Nu je er toch bent, check de events en plugins:

find app/code -name "events.xml" -exec grep -H "observer" {} \;
find app/code -name "di.xml" -exec grep -H "plugin\|preference" {} \;

Een <preference> die een core-class overschrijft is hoe een kleine content-aanpassing stilletjes de checkout herschrijft. Noteer ze allemaal. Hetzelfde geldt voor custom controller routes onder etc/frontend/routes.xml, die ongemerkt endpoints kunnen blootleggen die OWASP's standaard CSRF- en ACL-verwachtingen omzeilen.

Minuut 15 tot 25: cron, indexers, queues

De twee vragen die altijd worden overgeslagen: draait de cron eigenlijk, en zijn de indexers gezond?

crontab -l
ls -la var/log/cron.log var/log/magento.cron.log 2>/dev/null
tail -50 var/log/cron.log

Is crontab -l leeg voor de web-user, dan draait de shop al die tijd zonder cron. Reindex, cache flush, catalogprijsregels, transactionele e-mails, niets ervan is afgevuurd. Check de timestamps in var/log/cron.log. Is de laatste entry van november 2024, schrijf dat dan op in 18-punts.

Dan de indexers:

php bin/magento indexer:status

Elke rij moet Ready tonen met Update by Schedule of Update on Save. Alles wat al uren in processing hangt, is een dode indexer. Alles op Invalid betekent dat de schedule achterloopt. Op een shop met 40.000 SKU's en een kapotte catalog_product_price-indexer kunnen de prijzen in de storefront zes maanden oud zijn.

Gebruikt de shop de message queue (de meeste doen dat voor async e-mails en klantenimports), kijk dan even naar de backlog:

SELECT topic_name, COUNT(*) AS queued
FROM queue_message
GROUP BY topic_name
ORDER BY queued DESC
LIMIT 20;

Een miljoen onverwerkte async.operations.all-rijen is geen queue, dat is een kerkhof. Beslis voor de volgende deploy of je gaat draineren of truncaten, en doe nooit allebei tegelijk.

Minuut 25 tot 35: database-gezondheid en blootgestelde PII

Verbind read-only met de database als dat kan. De eerste drie queries zijn bot:

SELECT
  table_schema AS db,
  ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) AS mb
FROM information_schema.tables
WHERE table_schema = DATABASE()
GROUP BY table_schema;

SELECT
  table_name,
  ROUND((data_length + index_length) / 1024 / 1024, 1) AS mb,
  table_rows
FROM information_schema.tables
WHERE table_schema = DATABASE()
ORDER BY (data_length + index_length) DESC
LIMIT 20;

De gebruikelijke verdachten bovenaan: sales_order_grid, quote, quote_item, report_viewed_product_index, search_query. Heeft quote 14 miljoen rijen op een shop die 200 orders per maand doet, dan heeft de quote-cleanup job sinds Obama in het Witte Huis zat geen run meer gehad.

Dan de log-tabellen. Magento komt met meerdere die eindeloos doorgroeien als je ze niet opruimt:

SELECT COUNT(*) FROM customer_log;
SELECT COUNT(*) FROM customer_visitor;
SELECT MIN(last_visit_at), MAX(last_visit_at) FROM customer_visitor;

Een customer_visitor-tabel met 50 miljoen rijen en entries tot 2018 is een normale vondst op een niet-onderhouden shop. Het is ook een AVG-blootstelling als de vorige dev nooit een retentiebeleid heeft ingesteld. Noteer datums, niet alleen rij-aantallen.

Nu je toch in de database zit, scan op de voor de hand liggende credential-leaks:

SELECT path, value FROM core_config_data
WHERE path LIKE '%password%'
   OR path LIKE '%api_key%'
   OR path LIKE '%secret%';

Sommige zijn versleuteld, andere niet. Documenteer welke.

Minuut 35 tot 40: de kill-switch lijst

De laatste vijf minuten zijn het belangrijkst. Je repareert nog niets. Je schrijft op wat de shop vanavond offline zou halen als je het verkeerd aanraakt.

Open app/etc/env.php en noteer:

  • Database host: zelfde server, of een managed cluster?
  • Cache backend: Redis, Varnish, of in-memory?
  • Session backend: waar leven de live carts?
  • De crypt.key-waarde. Raak je die kwijt, dan kun je opgeslagen betaalgegevens van klanten of third-party credentials niet meer ontsleutelen.

Dan .htaccess. Magento levert er een lange. Zoek naar niet-standaard regels. Een snippet die er ongezien aan wordt geplakt, ziet er meestal zo uit:

<IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteCond %{REMOTE_ADDR} !^203\.0\.113\.
    RewriteRule ^admin/.*$ - [F,L]
</IfModule>

Alles wat maatwerk is (custom redirects, IP-allowlists, een 410-block op /admin) kopieer je woordelijk in je aantekeningen. Apache's eigen mod_rewrite-documentatie is het waard om open te houden tijdens het lezen, vooral als er flag-combinaties tussen zitten die je niet herkent.

Tot slot, kijk naar de admin-URL. Magento 2 randomiseert die standaard:

php bin/magento info:adminuri

Is de output /admin en staat de shop publiek op het internet, dan heeft de vorige dev de security-feature uitgezet, en de brute-force pogingen in var/log/exception.log vertellen je hoe dat is gegaan.

Wat de 40-minuten audit je oplevert

Aan het eind heb je een one-pager die er ongeveer zo uitziet:

  • Magento 2.3.5-p2 op PHP 7.2, beide end-of-life
  • 14 custom modules, 3 met eval-calls, 1 die uitgaat naar een onbekend IP
  • Cron heeft sinds november niet meer gedraaid
  • customer_visitor heeft 47M rijen, terug tot 2018
  • crypt.key staat in env.php, nergens een back-up van te vinden
  • Admin-URL is /admin, exception log toont 3.000 brute-force pogingen in 30 dagen

Dat is een offerte. Het is ook een gesprek met de klant dat niet langer is van "laat me een paar uur rondkijken en zien wat ik vind". Het is: hier is de stand van de shop, hier is de volgorde waarin ik de boel zou oplossen, hier is wat elke stap kost en waarom. Klanten betalen facturen die er zo uitzien. Ze gaan in discussie met facturen die dat niet doen.

Wat je vandaag kunt doen

Heb je een overgenomen Magento-shop in je takenlijst staan, begin dan niet met een fix. Doe de audit hierboven op een dinsdagochtend, schrijf de one-pager, en stuur die naar de klant voor je het admin-panel opent. De fix-lijst schrijft zichzelf zodra dat rapport bestaat.

Toen we Pier bouwden, kwamen we dit patroon zo vaak tegen dat de audit-queries hierboven nu als one-click playbooks draaien, tegelijk tegen de MySQL editor en SFTP van de gedockte site. De versiehistorie maakt de read-only regel afdwingbaar in plaats van een wens.

— Vragen —

Kan ik deze audit op een Magento 1-shop draaien?

Het meeste wel. Sla de indexer:status- en message-queue-commando's over (Magento 1 heeft die niet). De stappen voor modules, cron, database en .htaccess zijn allemaal van toepassing, en de security-bevindingen zijn meestal erger.

Wat als de vorige developer geen SSH-toegang heeft achtergelaten?

Vraag FTP- en database-credentials, en draai de filesystem-greps lokaal nadat je app/code en app/etc hebt binnengehaald. De CLI-commando's hebben web-equivalenten via de browser fallback van n98-magerun, maar SSH is sneller.

Moet ik setup:upgrade draaien zodra de audit klaar is?

Niet zonder database-snapshot en een staging-clone. setup:upgrade op een shop met kapotte custom modules kan de admin-toegang om zeep helpen. Eerst auditen, dan een snapshot, daarna upgraden op staging.