Eén regel met een variabele midden in een SQL-string kan je hele klantendatabase openzetten. In oude Magento-modules vinden we die regels nog steeds.
Eén regel met een variabele midden in een SQL-string kan je hele klantendatabase openzetten. In oude Magento-modules vinden we die regels nog steeds.
De core van Magento is goed beschermd tegen SQL-injectie. Het gevaar zit in custom modules, oudere extensies en snelle "even een query"-oplossingen. In dit artikel laten we zien hoe het misgaat, hoe het veilig hoort en hoe wij het in een audit opsporen.
Wat SQL-injectie is
SQL-injectie ontstaat als invoer van een gebruiker onderdeel wordt van een SQL-statement zonder dat de database het verschil kent tussen data en commando. Een aanvaller kan dan:
- Gegevens lezen die hij niet mag zien, zoals klantgegevens of admin-hashes.
- Gegevens wijzigen of verwijderen.
- In sommige configuraties verder doordringen in de server.
In de OWASP Top 10:2025 valt dit onder A05 Injection. Zie OWASP en je webshop.
Veilig werken: de volgorde van voorkeur
Magento biedt meerdere lagen. Gebruik ze in deze volgorde:
- Repository en SearchCriteria. Voor het ophalen van entiteiten het veiligst en meest upgrade-vast.
- Collections met
addFieldToFilter. De collection regelt de binding. - De
Zend_Db/Magento select-API met bindings. Voor lagere query's, metquoteIntoof bind-parameters. - Raw SQL. Alleen als het echt niet anders kan, altijd met bindings.
SearchCriteria en collections
Een filter via de service-laag:
$searchCriteria = $this->searchCriteriaBuilder
->addFilter('sku', $sku)
->create();
$products = $this->productRepository->getList($searchCriteria);
Hier komt $sku nooit als tekst in de SQL terecht. Een collection werkt vergelijkbaar:
$collection->addFieldToFilter('status', ['eq' => $status]);
De waarde wordt gebonden of correct gequote. Dit is de manier die we in nieuwe code verwachten.
Bindings en quoteInto
Als je een directe query nodig hebt, gebruik je de database-adapter met bind-parameters:
$connection = $this->resource->getConnection();
$select = $connection->select()
->from($this->resource->getTableName('mijn_tabel'))
->where('email = ?', $email);
$rows = $connection->fetchAll($select);
Het vraagteken wordt vervangen door een correct gequote waarde. Of met expliciete bindings:
$sql = 'SELECT * FROM ' . $table . ' WHERE email = :email';
$rows = $connection->fetchAll($sql, ['email' => $email]);
En voor losse stukken: $connection->quoteInto('email = ?', $email).
Gevaarlijke patronen
Dit is wat we in audits tegenkomen. Het ziet er onschuldig uit:
// Gevaarlijk
$sql = "SELECT * FROM mijn_tabel WHERE email = '" . $email . "'";
$connection->query($sql);
// Gevaarlijk
$select->where("status = " . $status);
// Gevaarlijk
$connection->query("DELETE FROM mijn_tabel WHERE id IN (" . implode(',', $ids) . ")");
In alle drie gaat ongevalideerde invoer rechtstreeks in de query. Zelfs als een waarde "alleen een getal" hoort te zijn, moet je het afdwingen met een cast of validatie en liever nog binden.
Sortering en kolomnamen
Hier gaan ervaren developers ook de fout in. Bindings werken voor waarden, niet voor kolomnamen of sorteerrichting. Een parameter als ?sort=price&dir=desc die je direct in ORDER BY zet, is een klassiek lek:
// Gevaarlijk
$select->order($request->getParam('sort') . ' ' . $request->getParam('dir'));
De oplossing is een allowlist:
$toegestaan = ['price', 'name', 'created_at'];
$sort = in_array($request->getParam('sort'), $toegestaan, true)
? $request->getParam('sort')
: 'created_at';
$dir = strtoupper($request->getParam('dir')) === 'ASC' ? 'ASC' : 'DESC';
Hetzelfde geldt voor tabelnamen, LIMIT-waarden en filteroperatoren. Een allowlist is het enige echte antwoord.
LIKE en IN
LIKE-zoekopdrachten vragen extra aandacht, omdat % en _ speciale tekens zijn. Binding beschermt tegen injectie, maar niet tegen een gebruiker die met wildcards je database zwaar belast. Escape daarom wildcards waar dat nodig is.
Bij IN-lijsten: bouw geen komma-gescheiden string met implode van ruwe invoer. Gebruik de ingebouwde IN-ondersteuning:
$select->where('id IN (?)', $ids);
Verstopte bronnen van invoer
Niet alle invoer komt uit een formulier. Denk ook aan:
- Request headers en cookies.
- Productattributen of CSV-importen die later in een query worden gebruikt.
- Data uit de database die eerder door een gebruiker is ingevoerd, en nu in een tweede query wordt gebruikt (second-order injection).
- Parameters van API-aanroepen. Zie API-beveiliging voor e-commerce.
Waarom oude modules vaker lekken
Oudere extensies zijn vaak geschreven voor Magento 1 of voor het begin van Magento 2, voordat bindings gemeengoed waren. Typische kenmerken:
- Directe SQL in controllers of blocks, soms zelfs in een
.phtml. - Gebruik van
ObjectManagerom een connectie te pakken. - Geen validatie op request-parameters.
- Een leverancier die niet meer bestaat.
Dit hoort bij het bredere onderwerp extensies en supply chain-risico. Een extensie zonder updates is een kwetsbaarheid die wacht. Overweeg vervanging of zelf onderhouden.
Hoe wij het opsporen in een audit
In een Magento security audit pakken we SQL-injectie systematisch aan:
- Statisch zoeken. We scannen op patronen:
->query(,->fetchAll(,->where(met stringconcatenatie,ORDER BYmet variabelen, enimplodein queries. Een gewonegrepbrengt al veel boven water:
grep -rnE "(query|fetchAll|fetchRow|fetchOne|where)\(.*\\quot; app/code/ --include=*.php
- Invoer volgen. Voor elke gevonden query: waar komt de waarde vandaan? Is het een request-parameter, header of database-veld?
- Handmatig testen op een testomgeving met een kopie van de data. Nooit op productie.
- Statische analyse in CI. PHPStan, Psalm en Semgrep met regels voor SQL-concatenatie vangen regressies. Zie ook veilig bouwen met een klein team.
- Databaserechten nakijken. De database-gebruiker van Magento hoeft geen rechten buiten de eigen database. Beperkte rechten verkleinen de schade als het toch misgaat.
Dit is gericht op bevindingen die je kunt oplossen, geen lange lijst met theoretische punten. Het bredere beeld vind je in wat een Magento security audit controleert.
Voorkomen in je eigen proces
- Regel in je coding standards: geen stringconcatenatie in queries, ook niet "even snel".
- Review elke query. Een pull request met raw SQL krijgt altijd een tweede paar ogen.
- Gebruik de service-laag waar mogelijk en raw SQL alleen voor rapportage of bulkwerk.
- Allowlists voor alles wat geen waarde is: kolommen, richtingen, tabellen.
- Minimale databaserechten per applicatie.
- Foutmeldingen niet tonen aan bezoekers. SQL-fouten in de browser helpen aanvallers enorm.
Wanneer is dit overkill?
Gebruik je alleen de standaard Magento en een paar goed onderhouden extensies, dan is dit risico klein. De moeite loont bij shops met veel maatwerk, oude modules, eigen koppelingen of een eigen zoek- en filterlaag. Een gerichte code-scan van de eigen modules kost dan weinig tijd en levert vaak direct resultaat.
Een voorbeeld van een veilige herschrijving
Neem een oude module die klanten zoekt op naam met een sorteeroptie. De onveilige versie:
$name = $this->getRequest()->getParam('name');
$sort = $this->getRequest()->getParam('sort');
$sql = "SELECT * FROM mijn_klanten WHERE name LIKE '%" . $name . "%' ORDER BY " . $sort;
$rows = $connection->fetchAll($sql);
Er zijn twee lekken: de zoekterm en de sortering. De veilige versie:
$name = (string) $this->getRequest()->getParam('name');
$toegestaan = ['name', 'created_at'];
$sort = in_array($this->getRequest()->getParam('sort'), $toegestaan, true)
? $this->getRequest()->getParam('sort')
: 'created_at';
$select = $connection->select()
->from($this->resource->getTableName('mijn_klanten'))
->where('name LIKE ?', '%' . $name . '%')
->order($sort . ' ASC');
$rows = $connection->fetchAll($select);
De waarde gaat via een binding, de kolom komt uit een allowlist. Het is niet meer code, alleen bewuster geschreven.
Wat je ziet als er misbruik is
SQL-injectie laat sporen na, als je weet waar je moet kijken:
- Ongewoon lange of vreemde waarden in de toegangslogs van endpoints met zoek- of filterparameters.
- Veel foutmeldingen van de database in een korte periode.
- Een plotselinge piek in zware queries zonder verklaring in je verkeer.
- Nieuwe of gewijzigde admin-gebruikers in de database zonder dat iemand ze heeft aangemaakt.
Heb je zo'n vermoeden, ga dan door naar Magento 2 gehackt: wat nu?. Maak eerst een kopie van logs en database voordat je opruimt.
Wat een WAF wel en niet doet
Een Web Application Firewall herkent veel gangbare injectiepatronen en blokkeert ze. Dat is waardevol als extra laag. Het heeft grenzen:
- Aanvallers kunnen payloads aanpassen om regels te ontwijken.
- Een WAF kent je applicatie niet. Een legitiem verzoek dat toevallig op een aanval lijkt, wordt soms geblokkeerd.
- Het vervangt geen reparatie van de code eronder.
Gebruik een WAF om tijd te winnen en ruis weg te filteren, en repareer de bron.
Veelgestelde vragen
Is Magento zelf kwetsbaar voor SQL-injectie?
De core is goed beschermd, maar er zijn in het verleden wel SQL-injectiekwetsbaarheden gevonden en gepatcht. Daarom blijft patchen belangrijk. Het grootste risico zit in custom modules en extensies.
Beschermt addFieldToFilter tegen SQL-injectie?
Ja, voor de waarde. De collection bindt of quote de waarde. De veldnaam moet je zelf in de hand houden en nooit rechtstreeks uit gebruikersinvoer halen.
Kan ik ORDER BY veilig maken met bindings?
Nee. Bindings werken voor waarden, niet voor kolomnamen of sorteerrichting. Gebruik een allowlist van toegestane waarden.
Hoe vind ik SQL-injectie in een bestaande shop?
Zoek in je eigen modules naar queries met stringconcatenatie, volg waar de variabelen vandaan komen en test op een kopie van de omgeving. Een security audit doet dit systematisch, inclusief extensies.
Is een WAF genoeg als bescherming?
Een WAF vangt veel standaardpogingen af, maar is geen vervanging voor veilige code. Aanvallers omzeilen WAF-regels regelmatig. Zie WAF en bot-bescherming als aanvulling.
Wil je dat wij je custom modules doorlichten op SQL-injectie en andere kwetsbaarheden? Bekijk onze Magento security-aanpak of neem contact op via /contact.

Geschreven door Ruthger Idema
15+ jaar ervaring in e-commerce development. Gespecialiseerd in Magento, Shopify en Laravel maatwerk.
Meer over ons team →


