XSS in Magento voorkomen: de $escaper goed gebruiken in templates
Terug naar blog

XSS in Magento voorkomen: de $escaper goed gebruiken in templates

Bijgewerkt 8 min leestijd

Een product-naam, een zoekterm of een klantnotitie die zonder escaping in een template belandt, is genoeg voor een XSS-lek.

Een product-naam, een zoekterm of een klantnotitie die zonder escaping in een template belandt, is genoeg voor een XSS-lek. Het is een van de meest voorkomende bevindingen in Magento-maatwerk.

Cross-site scripting (XSS) betekent dat een aanvaller JavaScript in jouw pagina kan laten draaien. In een webshop is dat ernstig: het script kan sessies stelen, formulieren aanpassen of op de checkout betaalgegevens afvangen. In het admin-paneel kan het zelfs leiden tot overname van de beheeromgeving.

Magento heeft een goede set escaping-functies. Het probleem is bijna altijd dat ze niet, of op de verkeerde plek, worden gebruikt.

Waarom escaping per context werkt

De kernregel: escape bij uitvoer, en kies de functie die past bij de plek waar de data landt. Dezelfde string vraagt in HTML, in een attribuut, in een URL en in JavaScript om een andere behandeling.

In een .phtml template gebruik je de $escaper:

FunctieGebruik voorVoorbeeld
escapeHtml()Tekst tussen HTML-tags

escapeHtml($name) ?>

escapeHtmlAttr()Waarde in een HTML-attribuut
escapeUrl()URL in href of src
escapeJs()Waarde in een JavaScript-stringvar x = 'escapeJs($v) ?>';
escapeCss()Waarde binnen CSSzeldzaam, maar bestaat

Een veelgemaakte denkfout: escapeHtml() overal gebruiken. Dat beschermt tekst tussen tags, maar niet automatisch een waarde binnen een onclick-attribuut of een script-blok. Daar heb je escapeJs() of beter, een andere aanpak nodig.

De fouten die we het vaakst zien

1. Helemaal geen escaping

php
<h2><?= $block->getTitle() ?></h2>

Als getTitle() data bevat die ooit door een gebruiker is ingevoerd, direct of indirect, is dit een lek. Dat geldt ook voor data uit een import, een ERP of een externe API. "Dat komt uit ons eigen systeem" is geen vrijbrief, want daar zit meestal ergens een invoerveld voor.

Het juiste patroon:

php
<h2><?= $escaper->escapeHtml($block->getTitle()) ?></h2>

2. Het verkeerde escape-type

php
<a href="<?= $escaper->escapeHtml($url) ?>">
escapeHtml laat javascript:alert(1) als URL staan. Gebruik escapeUrl() en valideer bovendien dat het schema http of https is als de bron onbetrouwbaar is.

3. Data in een inline script

php
<script>
    var customer = '<?= $block->getCustomerName() ?>';
</script>

Een naam met een quote en een breekt hieruit. Beter is de data als JSON serialiseren met de ingebouwde helpers, of de waarde in een data-attribuut zetten dat je met escapeHtmlAttr() beschermt en vanuit JavaScript uitleest.

4. HTML bewust toestaan zonder filtering

Soms wil je echt HTML tonen, zoals een CMS-blok of een productbeschrijving. Dan gebruik je escapeHtml() met een lijst toegestane tags, of laat je het via de Magento-filters lopen. Een ongefilterde echo van HTML uit de database is een open deur.

5. Vertrouwen op de browser of op de front-end

Validatie in JavaScript is geen beveiliging. Een aanvaller stuurt gewoon rechtstreeks een verzoek. De server moet zelf controleren en bij uitvoer escapen.

Admin-XSS: onderschat

Veel teams denken: "het admin-paneel is alleen voor ons, dus XSS daar is niet erg." Dat klopt niet. Een voorbeeld van een scenario:

  • Een bezoeker plaatst via het contactformulier of een orderopmerking een script.
  • Een medewerker opent die order of dat bericht in de admin.
  • Het script draait met de rechten van die medewerker, en kan bijvoorbeeld een nieuwe admin-gebruiker aanmaken of een script op de checkout plaatsen.

Dit heet stored XSS, en het is een bekende route naar volledige overname. Alle data die een klant kan invoeren en die in de admin verschijnt (adressen, opmerkingen, productreviews, formuliervelden), vraagt dezelfde escaping als de storefront.

Beperk de schade met 2FA, IP-beperking en rollen, zodat een overgenomen sessie minder kan.

Hyvä, Alpine en x-data

Op Hyvä-shops doet Alpine.js het werk van de oude JavaScript-laag. Dat is lichter en overzichtelijker, maar het brengt eigen valkuilen mee. Het gevaarlijkste patroon is PHP-data direct in een x-data of x-init attribuut plaatsen:

php
<div x-data="{ name: '<?= $block->getCustomerName() ?>' }">

Dit mengt twee contexten: een HTML-attribuut en een JavaScript-expressie. Een enkele quote in de naam breekt uit de string en voert code uit. Zelfs met escapeHtmlAttr() blijft de JavaScript-context in het attribuut een risico, omdat de browser eerst het attribuut decodeert en Alpine daarna het resultaat als code evalueert.

Veiligere werkwijze:

  • Serialiseer data als JSON via json_encode met veilige vlaggen en plaats die in het attribuut met escapeHtmlAttr().
  • Houd de logica in een Alpine-component en geef alleen data door, geen strings die code kunnen worden.
  • Gebruik x-text in plaats van x-html zodra je tekst toont. x-html rendert HTML en is een directe XSS-route bij onbetrouwbare data.

Hyvä heeft ook een CSP-compatibele aanpak voor Alpine, zonder unsafe-eval. Dat is een sterk argument om je templates daarop in te richten, omdat een strikte Content Security Policy dan haalbaar blijft. Meer over Hyvä.

CSP als vangnet, niet als oplossing

Een Content Security Policy beperkt wat een geïnjecteerd script kan doen. Het lost de XSS niet op. Gebruik het als tweede verdedigingslaag. Escaping blijft de basis.

Code review checklist

Neem deze punten mee bij elke review van een .phtml of ViewModel:

  • [ ] Staat er elke keer een $escaper->escape... rond data die niet 100% vast is?
  • [ ] Past de escape-functie bij de context (HTML, attribuut, URL, JS)?
  • [ ] Komen er variabelen in