Contao-News

Wir informieren Dich hier regelmäßig zu Updates, stellen Best-Practice-Arbeiten vor und berichten über Aktuelles aus dem Contaoversum.

Die Jagd auf überflüssige Cookies

von Yanick Witschi – Aktuelles

In Contao 4.8 werden wir erneut viele Verbesserungen im Zusammenhang mit Caching und Reverse Proxies vornehmen. Solltest du als reiner Anwender von Contao bei der Bezeichnung "Reverse Proxy" bereits ausgestiegen sein, lies bitte trotzdem noch ein paar Sätze weiter, denn dieser Blogpost ist auch für dich!

Der "Reverse Proxy" ist nichts anderes als das, was dir evtl. als "Server Cache" bekannt ist. Warum ich es nicht so nenne? Weil "Reverse Proxy" nun mal eine der Bezeichnungen ist, die offiziell dafür verwendet werden. Eine andere Bezeichnung, über die du womöglich irgendwann einmal stolpern könntest, wäre "HTTP Accelerator" oder "Gateway Cache".

Wie dem auch sei, wir sind bestrebt, Contao so nah wie nur möglich an den HTTP-Standard zu bringen und dafür zu sorgen, dass es eben hinter jedem dieser Reverse Proxies einwandfrei funktionieren kann. Die Frage nach dem "Warum?" lasse ich gerne gelten, schliesslich hat es doch bisher auch immer funktioniert, nicht wahr?

Natürlich dürfte eine eigens gebaute Lösung für die meisten kleineren bis mittleren Webseiten völlig ausreichen. Wer braucht schon einen leistungsfähigen Reverse Proxy à la Varnish vor dem eigenen Webserver? Wohl wahr. Aber hey, wäre es nicht toll zu wissen, dass egal welche Besucherzahl dich morgen erwartet, du jederzeit die Möglichkeit hast, einen leistungsstarken Proxy vorzuschalten? Völlig egal in welcher Programmiersprache dieser Proxy geschrieben ist und ob du nun selber dazu in der Lage bist oder deine Entwickler das tun müssen. Schon alleine das Wissen, dass sich Contao an den HTTP-Standard hält und es dadurch möglich wäre, halten wir für erstrebenswert.

Folglich haben wir über die vergangenen Versionen von Contao unsere eigene Lösung entfernt und fokussieren uns darauf, dass Contao die korrekten Caching-Informationen über die entsprechenden HTTP-Header mitliefert und somit mit jeglichen Reverse Proxies kompatibel ist. Damit auch alle "normalen" Webseiten weiterhin von einem "Server Cache" profitieren können, nutzen wir zurzeit standardmässig einen Reverse Proxy, der in PHP geschrieben ist und der vor Contao läuft. Dies ist der Symfony HTTP Cache. So können wir uns an die Standards halten, für den Betrieb hinter externen Reverse Proxies vorbereitet sein und trotzdem für "normale" Webseiten serverseitiges Caching anbieten. Eigentlich schlau soweit, nicht?

Nur leider haben wir ein paar Dinge eingebaut, die sich als nicht sonderlich praktisch erwiesen haben. Also um ehrlich zu sein: Ich habe sie eingebaut und die Lösung hat auch zwei Jahre lang einigermassen gut funktioniert, nur halt quasi ausschliesslich mit dem Symfony Reverse Proxy, was die ganze Übung der Standardisierung ein bisschen überflüssig macht (Stichwort: header-replay-bundle). Entsprechend haben wir uns vorgenommen, weiter daran zu arbeiten, die alte Lösung abzuschaffen und ich bin mir nun ziemlich sicher, dass Contao 4.8 die erste Version werden könnte, die sich so richtig ohne grössere Strapazen hinter einem solchen externen Proxy betreiben lässt.

Ja aber, Cookies?

Klingt gut, aber was hat das mit Cookies zu tun?

Toll, du hast weitergelesen, das freut mich! Denn jetzt kommen wir zum grossen Problem eines Reverse Proxys. Die Frage lautet: "Was genau darf ein Reverse Proxy denn cachen und wann darf er eine Anfrage aus dem Cache ausliefern?".

Natürlich gibt es auch erst mal jede Menge Regeln, auf die ich hier nicht eingehen werde. Ein grosses Problem aber stellen Cookies dar, denn ein Cookie enthält beliebige Informationen, über deren Auswirkungen auf die Applikation ein Reverse Proxy keine Aussage treffen kann. Ein Beispiel: Ein Reverse Proxy weiss nicht, dass das Cookie PHPSESSID die PHP-Session enthält und daher einen Benutzer authentifiziert. Es wäre aber wichtig zu wissen, denn wenn er in diesem Fall etwas aus dem Cache ausliefern würde, so könnte plötzlich Benutzer B den Inhalt von Benutzer A sehen, der vielleicht ein paar Sekunden vor Benutzer B auf der Seite war und den Cache-Eintrag überhaupt erst generiert hat.

Die Regel ist ganz einfach: Jedes Cookie kann potenziell einen Besucher identifizieren und somit könnte die Applikation (also Contao) personalisierte Inhalte (Warenkörbe, persönliche Empfehlungen, Logins etc.) ausliefern. Somit darf ein Reverse Proxy standardmässig auch nichts aus dem Cache ausliefern, wenn die Anfrage ein Cookie enthält.

Stop! Dies wäre die Stelle an der du darüber nachdenken solltest, was es denn so für Cookies gibt. Wir halten nochmal fest: Jedes einzelne Cookie bedeutet implizit, dass der Reverse Proxy umgangen wird. Wollen wir eine Antwort vom Reverse Proxy aus dem Cache generieren lassen, so müssen wir sicherstellen, dass kein einziges Cookie den Weg zum Reverse Proxy findet.

Macht ja nix, mir fällt ausser der PHPSESSID eigentlich sowieso kein Cookie ein.

Weit gefehlt. Hier ein paar Beispiele:

  • Das Contao CSRF-Cookie zum Verhindern von CSRF-Attacken
  • Die ganzen _ga_* Cookies von Google Analytics
  • Die ganzen _pk_* Cookies von Matomo
  • Wie wär's mit Cloudflare's __cfduid Cookie?
  • Dein cookiebar_accepted Cookie, um zu wissen ob die Cookie-Bestimmungen akzeptiert wurden?
  • usw.

Die Liste ist ziemlich lang und der aufmerksame Leser hat jetzt schon einen grundsätzlichen Unterschied zwischen z.B. dem PHPSESSID Cookie und z.B. dem _ga_* Cookie entdeckt. Denn eines davon ist wirklich relevant für den Server (also die PHP-Session) und das andere ist für das clientseitige Tracking (also innerhalb des Browsers) des Benutzers zuständig und für Contao völlig irrelevant.

Mit anderen Worten, wenn ein Request nur ein _ga_* Cookie enthält, können wir trotzdem die Antwort aus dem Cache liefern, denn dieses Cookie generiert keinen persönlichen Inhalt.

Ein schlauer Reverse Proxy könnte also nur die relevanten Cookies berücksichtigen und alle anderen wegschmeissen, bevor er entscheidet, ob eine Antwort aus dem Cache geliefert werden kann. Und genau dahin führt uns der Weg und genau hier brauchen wir die Hilfe aller Benutzer und Entwickler.

Denn es ist so: Für viele Contao-Nutzer wird dieser Blogpost womöglich trotz meinen grössten Anstrengungen immer noch nicht verständlich sein. Das macht auch weiter nichts und ist völlig in Ordnung. Es bedeutet aber im Umkehrschluss auch, dass wir die Entscheidung, welche Cookies für eine Applikation wirklich relevant sind und welche nicht, nicht den Nutzern überlassen können. Für die meisten wäre es wohl undenkbar, eine Whitelist (eine Liste der relevanten Cookies) für ihre Applikation zu pflegen. Vor allem dann nicht, wenn sie sich noch 15 Erweiterungen zu Contao installieren, die alle auch noch ein Cookie brauchen und somit ebenfalls auf diese Whitelist gesetzt werden müssten.

Daher haben wir uns dafür entschieden, standardmässig eine Blacklist (also eine Liste der garantiert irrelevanten Cookies) mitzuliefern. Für die etwas fortgeschritteneren Anwender kann die Whitelist natürlich definiert werden, sie übersteuert in diesem Fall dann die Blacklist.

Wir brauchen nun folgendes von dir: Schau dir deine Contao-Webseiten an und überprüfe mithilfe der Entwicklertools, welche Cookies auf diesen Webseiten gesetzt werden. Versuche zu entscheiden, ob diese wirklich relevant für den Server sind oder ob sie ignoriert werden können. Lass uns von den entsprechenden Cookies, von denen du denkst, sie könnten zur Blacklist hinzugefügt werden, in meinem Pull Request wissen. So bekommen wir eine umfangreiche Blacklist und können die Chancen auf eine Auslieferung aus dem Cache (einen sogenannten "Cache-Hit") erhöhen.

In vielen Fällen wird die Lösung auch ein Ticket beim Entwickler der entsprechenden Erweiterung sein, denn Cookies werden leider oft eingesetzt, obwohl es gar nicht nötig wäre. Und das ist auch schon der Übergang zum Abschnitt für Entwickler. Wenn du ein reiner Anwender bist, dann darfst du natürlich gerne weiterlesen, aber ab jetzt wird es noch etwas technischer.

Danke für deine Mithilfe!

Für Entwickler

Du hast ein Ticket erhalten, um ein Cookie loszuwerden? Gut, denn genau das ist das, war wir hier erreichen wollen: Eine Jagd auf sinnlose Cookies!

Cookies wurden traditionell eingesetzt, weil es keine andere Möglichkeit gab, benutzerspezifische Daten clientseitig zu speichern. Aber mit JavaScript und der Web Storage API (localStorage bzw. sessionStorage) und der IndexedDB API stehen uns schon seit geraumer Zeit Alternativen zur Verfügung. Nachfolgend werde ich ein paar typische Fälle und einen möglichen Lösungsweg aufzeigen, wie du das Cookie in diesem Fall loswerden kannst:

  • Die CookieBar - der Klassiker. Du speicherst in einem Cookie, ob ein Besucher deine Cookie-Bedingungen bereits akzeptiert hat oder nicht, damit du beim nächsten Seitenaufruf die CookieBar eben nicht mehr anzeigen musst. Das wäre ein perfektes Beispiel für localStorage. Der Server braucht das nicht zu wissen, denn den Inhalt der CookieBar kann man auch mit JavaScript dynamisch in den DOM einfügen lassen, wenn der entsprechende Key noch nicht im localStorage existiert.

  • Die Sortierreihenfolge in einer Tabelle, damit beim nächsten Seitenaufruf immer noch die gleiche Spalte alphabetisch sortiert ist? Hallo localStorage!

  • Die Scroll-Position an der sich der User befand bevor er das Formular abgeschickt hat? Hallo sessionStorage (oder localStorage wenn die Position über Browser-Tabs hinweg geteilt werden soll)!

  • Overlay-Grössen die man per Drag and Drop verändern kann und die gespeichert bleiben sollen? Hallo localStorage!

  • Eine Liste von bereits geklickten Links temporär speichern und suchen, ob ein bestimmter Link bereits geklickt wurde? Hallo IndexedDB!

  • Dark-Theme oder Light-Theme und je nach Wahl wird dynamisch das eine oder andere CSS nachgeladen? Die Auswahl wiederum im localStorage speichern und entweder beide Styles laden und z.B. über eine CSS-Klasse im <body> regeln. Oder für die Daten-Sparfüchse: Warum das gewünschte light.css oder dark.css nicht per Ajax nachladen?

Du siehst, dass in vielen Fällen die lokalen JavaScript APIs genau das sind, was du suchst und du kein Cookie brauchst. Aber Vorsicht: Daten lokal zu speichern ist nicht sicher! Nutze sie niemals, um sensible Daten zu speichern. Also bitte keine Session-IDs, JWTs, Benutzerdaten, Kreditkartendaten oder API-Schlüssel etc. lokal speichern!

Aber es gibt auch Fälle, in denen ist ein Cookie die richtige Wahl. Ein Beispiel wäre etwa ein Gästewarenkorb für Produkte. Der Benutzer ist nicht eingeloggt, also haben wir keine Session und noch wenn wir eine generieren würden, so wäre deren Laufzeit mit grosser Wahrscheinlichkeit zu kurz für unseren Warenkorb. Folglich ist ein separates Cookie mit separater Laufzeit die richtige Wahl und es ist richtig, dass in einem solchen Fall der Cache umgangen wird. Wer in diesem Beispiel besonders viel Energie hat, könnte es lösen indem er/sie den Warenkorb per Javascript/Ajax von einem eigenen Endpoint (z.B. /guest_cart) lädt, das Cookie auch auf diesen Pfad einschränkt und danach den Inhalt an der gewünschten Position in der Seite einfügt. So würden alle anderen Anfragen ohne Cookie stattfinden. Das Cookie selbst würde nur bei /guest_cart mitgeliefert werden.

Diese Idee ist übrigens auch nicht neu und könnte mit hinclude ohne eine Zeile JavaScript zu schreiben gelöst werden.

Meistens gibt es mehrere Lösungen, im Zweifelsfall einfach schnell auf Twitter das GitHub-Issue (o.ä.) verlinken und ich schaue vorbei und sehe, ob ich helfen kann! :-)

Und jetzt lasst uns unnötige Cookies verputzen!

Alle News anzeigen

Kommentare

Kommentar von Johannes Pichler |

Wie setzt du das dann um. Werden Cookies der Blacklist vor dem Sym.Cache aus dem request gelöscht?

Antwort von Yanick Witschi

Richtig, der Proxy löscht die entsprechenden Cookies bevor er den Cache-Lookup versucht. Das ist auch bei anderen Proxies so üblich womit dieses Konzept auch funktionieren würde (siehe z.B. Varnish: https://varnish-cache.org/docs/6.2/users-guide/increasing-your-hitrate.html#cookies).

Kommentar von Marcus Lelle |

Wow, Yanick, das klingt nach ner Menge Arbeit auf Deiner Seite.
Danke dafür!

Gruß Marcus

Kommentar von Thomas Bantel |

Manche meiner (Session-)Cookies sind für den Server nur auf einigen wenigen Seiten relevant. Auf den Seiten brauche ich sie aber und kann auf das Cookie deshalb auch nicht verzichten. Da sie die Serverantwort beeinflussen, darf ich sie eigentlich nicht per Black-/Whitelist als irrelevant einstufen. Somit würde keine einzige Seite der Website jemals gecacht, obwohl vielleicht nur 5% wirklich nicht gecacht werden dürfen. Wäre es hier eine gute und auch sichere praktische Lösung, die entsprechenden Cookies per Whitelist trotzdem als irrelevant einzustufen und die entsprechenden Seiten dann in der Seitenstruktur komplett vom Cache auszunehmen? Welche Alternativen gäbe es sonst noch, wenn Javascript dabei keine Rolle spielen darf?

Antwort von Yanick Witschi

Unter der Voraussetzung, dass nichts geändert werden und JavaScript keine Rolle spielen darf, ist deine Seite de-facto für einen Reverse Proxy nicht cachebar. Die einzige Alternative wäre alles was das Cookie potenziell braucht unter dem gleichen Pfad zu vereinen. Also bspw. alles nach domain.de/protected/foobar.html, domain.de/protected/baz.html etc. Dann das Cookie auf den Pfad /protected einschränken, dann wird es nur bei allen Unterseiten von /protected (und natürlich /protected selbst) mitgeschickt und der ganze Rest der Seite bleibt davon unbehelligt.

Kommentar von Hans Weber |

Cookies ohhhh, aber Hut ab Yanick für diesen Post. In der Ausführlichkeit und im Detail habe ich bisher noch nichts zu dem Thema gelesen. Interessant war vor allen auch der Ausblick was sich da in der Zukunft für Möglichkeiten bieten werden. Die Panik die derzeit um DSGVO und Cookies gemacht wird hat so manchen derart verunsichert, dass vieles auf Blacklisten landet was unnötig ist. Das Ende von Analytics und Co,. wird es garantiert nicht sein, denn ohne genaue Analyse der Seite und deren Besuchern wäre ja auch keine diesbezügliche Verbesserung erreichbar.

Einen Kommentar schreiben

Was ist die Summe aus 4 und 8?