On the way to master CSP in Magento2

/ Magento - Zurück zur Übersicht

Magento unterstützt seit mehreren Versionen mit dem Modul magento/module-csp die Konfiguration der Content-Security-Policy (CSP). Was bedeutet das für dich als Shopbetreiber? Du kannst festlegen, welche Skripte, Styles und andere Ressourcen genutzt werden dürfen, und woher sie stammen. Idealerweise sollten selbst Skripte, die einfach mit einem <script>-Tag inline eingebunden werden, vermieden werden, da sie potenziell über andere Angriffsmethoden in die Seite eingeschleust werden können. Für Shops, die Online-Zahlungen anbieten, ist das besonders kritisch, da hier strenge PCI-Compliance-Anforderungen gelten.

 

Unsere Reise beginnt mit der offiziellen Dokumentation von Adobe, die eine gute Basis bietet, um sich mit diesem Thema vertraut zu machen: https://developer.adobe.com/commerce/php/development/security/content-security-policies/.

Viel Theorie, aber wie setzen wir das praktisch um? Ein erster Schritt besteht darin, zu verstehen, welche Skripte aufgerufen werden. Als Shopbetreiber kannst du das grob einschätzen (z.B. durch Trackings und Trust Badges), aber im Shop selbst bleibt die Konfiguration oft unklar. Als Entwickler ist die Sichtweise oft genau umgekehrt.

Schritt 1:

Grundrauschen herausfiltern

Zunächst musst du den Report-Modus in Magento aktivieren. Dadurch erhältst du als Entwickler oder jeder, der die Entwicklerkonsole öffnet, einen Überblick darüber, was nicht konfiguriert ist. Dabei fällt auf, dass viele unsafe-inline-Hinweise auftreten. Laut der Dokumentation kannst du die einzelnen Skripte hashen oder einen Nonce verwenden – letzterer wird am besten über Magentos $secureRenderer automatisch hinzugefügt. Aber jedes Skript zu hashen? Das ist aufwendig und problematisch, da dynamische Inhalte aus PHP im Skript eingearbeitet sein können. Eine Änderung durch einen Redakteur kann den Check brechen, was einen neuen Deployment-Vorgang erforderlich macht.

Also probieren wir es anders. Lassen wir Magento alle Nonce-Settings übernehmen, was grundsätzlich funktioniert. Allerdings steht dies im Widerspruch zum Ansatz eines Full-Page-Caches in Magento, da der Nonce pro Anfrage neu erstellt werden sollte, aber im FPC gespeichert wird. Damit bleibt uns in einem ersten Schritt nur, unsafe-inline in einer csp_whitelist.xml-Datei zu erlauben (für Skript- und Style-Tags):

Nicht optimal, aber ein Schritt in die richtige Richtung für eine solide Basis.

Handling von unsafe-inline, Hashes und Nonces im FPC-Kontext

Dabei fällt auf, dass viele unsafe-inline-Hinweise auftreten. Laut der Dokumentation kannst du die einzelnen Skripte hashen oder einen Nonce verwenden – letzterer wird am besten über Magentos $secureRenderer automatisch hinzugefügt.

Selbst hashen und manuell hinzufügen:
($whitelistHash = base64_encode(hash('sha256', $content, true));) 

Aber jedes Skript zu hashen? Das ist aufwendig und problematisch, da dynamische Inhalte aus PHP im Skript eingearbeitet sein können. Eine Änderung durch einen Redakteur kann den Check brechen, was einen neuen Deployment-Vorgang erforderlich macht.

Also probieren wir es anders. Lassen wir Magento alle Nonce-Settings übernehmen, was grundsätzlich funktioniert. Allerdings steht dies im Widerspruch zum Ansatz eines Full-Page-Caches in Magento, da der Nonce pro Anfrage neu erstellt werden sollte, aber im FPC gespeichert wird. Damit bleibt uns in einem ersten Schritt nur, unsafe-inline in einer csp_whitelist.xml-Datei zu erlauben (für Skript- und Style-Tags):

 

<?xml version="1.0"?>
<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Csp/etc/csp_whitelist.xsd">
   <policies>
       <policy id="script-src-elem">
           <values>
               <value id="its-a-me-unsafe" type="host">'unsafe-inline'</value>
           </values>
       </policy>
       <policy id="style-src-elem">
           <values>
               <value id="its-a-me-unsafe" type="host">'unsafe-inline'</value>
           </values>
       </policy>
   </policies>
</csp_whitelist>

Nicht optimal, aber ein Schritt in die richtige Richtung für eine solide Basis.

 

Schritt 2:

Grundsetup verwendeter Skripte

Jetzt geht es darum, jedes Skript durchzugehen, in der csp_whitelist.xml zu registrieren und eventuell zu deployen, bevor wir zum nächsten übergehen.

 

Schritt 3:

Reporting aufsetzen

Hast du einmal alle Skripte durchgecheckt und keine Warnungen mehr erhalten, heißt es eine Woche warten. Es kann leicht passieren, dass neue Skripte auftauchen oder du möglicherweise eine CMS-Seite übersehen hast. 

Der CSP-Standard erlaubt die Definition einer Report-URI, die auch in der Magento-Konfiguration gesetzt werden kann. Hier gibt es spezielle Magento-Module, die diese setzen und direkt an sich selbst reporten, oder externe Services, die diese Ereignisse aufzeichnen.

Im Magento-Umfeld hat sich Sansec mit seinem Security Scanner einen Namen gemacht und bietet jetzt kostenlos einen Report-Endpunkt für CSPs an

(https://sansec.io/watch). Hier werden alle vom Browser gesendeten Events registriert, kategorisiert und stehen dir zur Verfügung – inklusive dem benötigten Eintrag für unsere csp_whitelist.xml.

 

Zusätzlich kann ein entsprechender Eintrag erlaubt werden und wir erhalten dann im unteren Teil der Seite die benötigten csp_whitelist.xml Einträge zum kopieren in unser Projekt geliefert:

Dieser Schritt kann auch an den Shopbetreiber delegiert werden. Wer weiß schließlich besser, was im Shop eingebunden ist?

Wie und was genau der Browser reportet, kannst du hier nachlesen: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/report-to. Es ist wichtig zu beachten, dass auch kundenspezifische Skripte, wie beispielsweise jene, die über Browser-Erweiterungen hinzugefügt werden, im Bericht auftauchen können, obwohl sie im Shop selbst nicht eingebunden sind. Diese müssen durch eine manuelle Überprüfung der betroffenen Seite identifiziert und gegebenenfalls ignoriert werden. Zudem ist es von Bedeutung, wie häufig berichtet wird und wann das Skript zuletzt gesehen wurde.

 

Was passiert, wenn der Shopbetreiber nicht regelmäßig nachkontrolliert? Das ist kein Problem – er erhält eine E-Mail-Benachrichtigung, sobald ein neues Skript gefunden wurde.

Es ist ebenfalls wichtig zu bedenken, dass Browser diese Informationen möglicherweise nicht sofort übermitteln (wenn sie das überhaupt tun) und die Daten außerdem nicht vollständig sein müssen.

 

Schritt 4:

Feedbackschleifen und CSP-Updates verkürzen

Sansec bietet eine Schnittstelle an, um erlaubte Skripte abzufragen und diese in den Shop zu übernehmen. integer_net stellt auf GitHub ein entsprechendes Modul zur Verfügung (https://github.com/integer-net/magento2-sansec-watch) das regelmäßige Updates der CSP ermöglicht. 

So kann der Shopbetreiber erlaubte Skripte quasi in Echtzeit ändern, ohne einen neuen Deployment-Prozess anstoßen zu müssen.

 

Schritt 5:

Darf's ein bisschen weniger sein?

Magento wurde für mehrere Shops entwickelt, und jeder Shop hat individuelle Anforderungen. Daher ist es sicherheitstechnisch wünschenswert, so wenige Skripte wie möglich zuzulassen, um potenzielle Sicherheitslücken zu minimieren. 

Der eine Shop ist eine hippe Jugendmarke und bindet die krassesten Social Media Plattformen ein, der andere gerade so mal die gute alte gediegene Craigslist? Aus Sicherheitssicht ist es wünschenswert, so wenige Skripte zu erlauben wie möglich, um potenzielle Injections (siehe einen Breach bei polyfill.io 2024) zu reduzieren. Magento bietet verschiedene Konfigurationsmöglichkeiten über csp_whitelist.xml hinaus:

Konfigurationsmöglichkeiten abseits der csp_whitelist.xml

Jedes Modul kann einen neuen PolicyCollector über das di.xml contributen und neue FetchPolicies zur Verfügung stellen, auch Magento stellt mehr als nur die XMLs zur Verfügung:

ConfigCollector: kann genutzt werden um Konfigurationswerte Policies für ganze Bereiche

oder für einzelne Controller zu definieren

  • CspWhiltelistXmlCollector: das ist worüber ein jeder spricht
  • ControllerCollector: über das Interface CspAwareActionInterface wird die Methode modifyCsp aufgerufen welche Modifizierungen für genau diesen Controller erlaubt
  • DynamicCollector: kann in z.B. einer Blockklasse oder phtml genutzt werden um dort die entsprechenden Policies zu setzen

 

Damit lassen sich zahlreiche Anwendungsfälle abdecken, insbesondere mit dem DynamicCollector. Doch was ist, wenn ich meine CSP-Einstellungen nicht in einem Block oder einer phtml-Datei für meine Integration verstecken möchte?

Eigenen PolicyCollector erstellen

Hier ein kleines Beispiel um zu zeigen wie einfach ein entsprechender PolicyCollector erstellt werden kann, welcher auf Konfigurationswerte horcht und meine Einstellungen hinzufügt, oder eben auch nicht:

class ModuleCspCollector implements PolicyCollectorInterface

{
   public function __construct(
       private DataHelper $dataHelper
   ) {
   }

   public function collect(array $defaultPolicies = []): array
   {
       $policies = $defaultPolicies;
       if(!$this->dataHelper->isActive()) {
           return $policies;
       }
       $policies[] = new FetchPolicy(
           'script-src',
           false,
           ['https://module.csp-script.com']
       );
       return $policies;
   }
}
<type name="Magento\Csp\Model\CompositePolicyCollector">
   <arguments>
       <argument name="collectors" xsi:type="array">
           <item name="my-module" xsi:type="object" sortOrder="99">ModuleCspCollector \Proxy</item>
       </argument>
   </arguments>
</type>

 

Ein weiterer Einsatz wäre eine Erweiterung der csp_whitelist.xml um Scope-Daten um eine granulare Einstellung pro Website/Store/StoreView zu gestalten. Hierzu kann man das Github Modul https://github.com/brosenberger/module-scoped-csp verwenden:

<?xml version="1.0" encoding="UTF-8"?>
<csp_whitelist xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:module:BroCode_ScopedCsp:etc/scoped_csp_whitelist.xsd">
        <policies>
            <policy id="img-src">
                <values>
                    <value id="data-brocode" scopeType="website" scopeCode="base" type="host">https://brocode.at</value>
                    <value id="data-other-brocode" scopeType="website" scopeCode="otherbase" type="host">https://other.brocode.at</value>
                </values>
            </policy>
        </policies>
</csp_whitelist>
 

 

Schritt 6:
Outlinen von Inlined Skripten und Styles

Wenn unsafe-inline problematisch ist und das Hashing unpraktisch, was bleibt dann? Eine Möglichkeit besteht darin, keine Skripte mehr inline zu verwenden. Logik in einem solchen JavaScript-Snippet sollte über die gängigen Magento-Methoden (zB require-js oder Alternativen von beispielsweise Hyvä) eingebunden werden. Das mag umständlich erscheinen, doch in den meisten Fällen ist dies der sicherste Weg.

Das ist jedoch äußerst mühsam und oft für ein einfaches `console.log('warum?')` übertrieben! Ich möchte meine PHP-Werte in das Skript einfügen, was sich über die Parameter bei einem `x-magento-init` als äußerst kompliziert gestaltet. Magento verwendet doch ebenfalls immer den `$secureRenderer` und berücksichtigt dabei nicht, ob es passt oder nicht! … Es gibt noch viele weitere Überlegungen dazu. Was wäre also die Lösung?

 

Hier bietet sich der SecureHtmlRenderer an, der in Magento größtenteils verbreitet ist oder in Zukunft genutzt werden sollte. Im Wesentlichen kümmert sich dieser um die „JavaScript-Magic“ für das Erstellen von Event-Listenern und das Überprüfen von Styles. Er übergibt alles an die Methode `renderTag()`, die schließlich in der Klasse `\Magento\Csp\Helper\InlineUtil` landet. Dort wird ein Nonce/Hash generiert und in die CSP-Header über den DynamicCollector eingefügt. Wenn Inline-Skripte problematisch sind, warum nicht den vorhandenen Inhalt als Datei im Theme auslagern?

public function aroundRenderTag($subject,
                               callable $proceed,
                               string $tagName,
                               array $attributes,
                               ?string $content = null,
                               bool $textContent = true): string
{
   if ($tagName == ‘script’) {
       // save content to disk with unique name
       // proceed original function with included script-src-attribute
       return $proceed($tagName, $adaptedAttributes)
   }

   return $proceed($tagName, $attributes, $content, $textContent);
}

(Quelle Script-Inhalt: https://github.com/Smile-SA/elasticsuite/blob/2.11.x/src/module-elasticsuite-tracker/view/frontend/templates/variables/page.phtml)

Im Frontend gibt es nun keine Script-Tags mehr, sondern lediglich includes von kleinen JS-Snippets:

Hier muss erwähnt werden, dass weitere Verfeinerungen möglich sind, wie die Komprimierung von diesen ausgelagerten Skripten und Styles, sowie das Hinzufügen von SRI-Attributen (SRI = Subresource Integrity - developer.mozilla.org/de/docs/Web/Security/Subresource_Integrity).

Somit kann dann im Anschluss der unsafe-inline CSP Eintrag entfernt werden, oder?
Alle nutzen den $secureRenderer, oder? Oder auch nicht. Wie kann man nun entsprechende Teile im phtml refactoren?
 

Beide Strategien funktionieren gut, zweitere für etwas größere hat den großen Nachteil, dass diese Teile des Codes sehr unleserlich werden können wenn zu viel Logik vorhanden ist (insbesondere wenn mittels PHP-If-Konstrukten teile des JS nicht geschrieben werden sollen).

(Quelle: https://github.com/Smile-SA/elasticsuite/blob/2.11.x/src/module-elasticsuite-tracker/view/frontend/templates/config.phtml)

Hier gibts es wiederum zwei Möglichkeiten, jedoch wenn man in die Verlegenheit kommt darüber nachzudenken, sollte man anstreben das Skript so oder so als eigene JS-Datei einzubinden:

eine Variable, viele Zeilen und .=

 

(Quelle: https://github.com/justbetter/magento2-sentry/blob/master/view/frontend/templates/script/sentry_init.phtml)

und dem Inhalte von script_init.phtml:

Schritt 7:

Mein Shop – Meine CSP-Festung

Mit diesen detaillierten Schritten kannst du deinen Shop in einen Zustand bringen, der den Sicherheitsanforderungen gerecht wird. Während die Anforderungen derzeit möglicherweise noch nicht sehr streng sind, haben viele Shops bereits bei ersten Verschärfungen im Checkout große Probleme gehabt. Aktiviere in diesem letzten Schritt den Strict-Mode und fordere den Browser damit auf, alle nicht explizit erlaubten Skripte zu ignorieren, um keine Kompromisse bei solchen Angriffen einzugehen!

Jetzt starten

Unsere Expert:innen unterstützen Dich gerne mit individueller Beratung für Deinen E-Commerce – skalierbar, zukunftssicher und auf Augenhöhe.