Technik · Anspruchsvoll

Kundenindividuelle Preise im Cache: wenn eine schnelle Website den falschen Preis zeigt

Ein Seiten-Cache speichert eine Antwort und liefert sie an viele Besucher aus. Enthält diese Antwort einen kundenindividuellen Preis oder Kontodaten, bekommt sie auch der nächste Besucher.

7 Min. Lesezeit

Reihen von Servern mit Netzwerkkabeln in einem Rechenzentrum.

Foto: Derrick Coetzee, gemeinfrei (CC0)

In diesem Ratgeber

Wie ein Seiten-Cache den Preis eines Kunden an einen anderen ausliefert

Ein Cache speichert eine Antwort unter einem Cache-Schlüssel und gibt die gespeicherte Kopie auf jede spätere Anfrage zurück, die denselben Schlüssel ergibt. Standardmäßig wird der Schlüssel aus der Anfragemethode und der URL gebildet: Schema, Host, Pfad und Query-String.

Nehmen wir einen B2B-Shop, in dem Kunde A einen vertraglichen Rabatt hat. A ist angemeldet und ruft eine Produktseite auf. Die Anwendung liest das Session-Cookie und erzeugt die Seite mit dem Preis von A. Ein Cache vor der Anwendung speichert diese Antwort unter dem Schlüssel für die Produkt-URL. Kunde B ruft dieselbe URL auf. Der Schlüssel ist identisch, also liefert der Cache die gespeicherte Seite aus, mit dem Preis von A und vielleicht dessen Firmennamen, ohne die Anwendung zu fragen.

Die Anwendung hat den richtigen Preis berechnet, und der Cache hat getan, wofür er konfiguriert war. Der Fehler liegt darin, dass die Antwort von etwas abhing, das im Schlüssel nicht enthalten war: wer gefragt hat. Jedes Leck dieser Art hat diese Form. Eine Antwort unterscheidet sich nach einer Dimension, etwa Kunde, Preisliste oder Währung, und im Schlüssel fehlt sie.

Gemeinsame Caches und der private Browser-Cache

  • Ein gemeinsamer Cache (Shared Cache) speichert Antworten für viele Nutzer: ein CDN, ein Reverse Proxy vor dem Webserver oder ein Seiten-Cache im Server oder in der Anwendung.
  • Ein privater Cache gehört einem einzelnen Nutzer. In der Praxis ist das der Browser-Cache.

Eine persönliche Antwort in einem gemeinsamen Cache ist ein Leck zwischen Kunden. Im Browser-Cache bleibt sie bei diesem Nutzer, kann aber nach der Abmeldung trotzdem wieder erscheinen, etwa über die Zurück-Schaltfläche an einem Rechner, den mehrere Personen nutzen.

HTTP bringt einen einzigen Schutz mit: Die Antwort auf eine Anfrage mit einem Authorization-Header darf von einem gemeinsamen Cache nicht wiederverwendet werden, sofern die Antwort es nicht ausdrücklich erlaubt. Für Cookies gibt es keinen solchen Schutz. Einem Cache sagt ein Session-Cookie nichts. Die meisten Shops und Portale halten die Anmeldung in einem Cookie, also muss die Absicherung aus Ihren eigenen Headern und Ihrer Cache-Konfiguration kommen.

Seiten-Caches, die in Anwendungen und Server-Module eingebaut sind, folgen oft eigenen Regeln, etwa „nicht cachen, wenn ein Login-Cookie vorhanden ist“. Verwendet Ihre Anmeldung ein Cookie, das der Cache nicht kennt, greift diese Regel nie.

Die HTTP-Header, die entscheiden, wer eine Antwort speichern darf

Der Ursprungsserver teilt den Caches über Cache-Control in der Antwort mit, was sie tun dürfen.

  • private: Die Antwort ist für einen einzelnen Nutzer bestimmt. Ein gemeinsamer Cache darf sie nicht speichern, der Browser schon.
  • public: Jeder Cache darf die Antwort speichern, auch eine, die sonst nur privat gespeichert werden dürfte.
  • no-store: Kein Cache, ob gemeinsam oder privat, soll die Antwort speichern.
  • no-cache: Die Antwort darf gespeichert werden, muss aber vor jeder Wiederverwendung beim Ursprungsserver neu geprüft werden.
  • s-maxage: wie viele Sekunden die Antwort in gemeinsamen Caches als frisch gilt; dort hat der Wert Vorrang vor max-age. Browser ignorieren ihn.

Eine Antwort ohne eine dieser Angaben ist nicht automatisch sicher. Caches dürfen eine Gültigkeitsdauer schätzen, wenn der Ursprungsserver keine angibt, und viele Proxys und CDNs sind mit Regeln konfiguriert, die die Header des Ursprungsservers übersteuern. Eine Regel „alles cachen“, die einen Shop schneller machen sollte, kann ein korrektes Cache-Control: private wirkungslos machen.

Achten Sie auch auf Set-Cookie. Speichert ein gemeinsamer Cache eine Antwort, die ein Session-Cookie setzt, können spätere Besucher dieses Cookie erhalten. Antworten, die eine Session beginnen oder ändern, sollten no-store tragen.

Warum Vary: Cookie ein grobes Werkzeug ist

Der Antwort-Header Vary nennt Anfrage-Header, die Teil des Schlüssels werden. Vary: Cookie sieht also nach der Lösung aus: eine Kopie pro Cookie-Wert.

Eine gute ist es selten. Der Cache vergleicht den gesamten Cookie-Header, und der enthält auch Analyse- und Einwilligungs-Cookies, die bei fast jedem Besucher anders sind. Anonyme Besucher, die sich alle eine Kopie teilen könnten, bekommen jeder eine eigene, und die Trefferquote bricht ein. Persönliche Seiten liegen weiterhin in einem gemeinsamen Cache, eine Kopie pro Kunde. Und manche CDNs unterstützen Vary nur für bestimmte Header. Lesen Sie also die Dokumentation Ihres CDN und testen Sie.

Session-Grenzen: die Seite teilen, nicht den Cache

Der verlässliche Entwurf trennt das, was für alle gleich ist, von dem, was persönlich ist, und sendet jeden Teil mit seiner eigenen Richtlinie.

  1. Erzeugen Sie die Seite ohne alles Persönliche: Produktdaten, Bilder, Beschreibungen und entweder der Listenpreis oder kein Preis. Senden Sie sie mit public und s-maxage, damit der gemeinsame Cache sie an alle ausliefern kann.
  2. Laden Sie die persönlichen Teile in einer eigenen Anfrage nach, sobald die Seite da ist: Kundenpreis, Kontoname, Anzahl der Artikel im Warenkorb. Senden Sie diese Antworten mit private, no-store.
  3. Prüfen Sie die Session bei jeder persönlichen Anfrage auf dem Server.

Hängen die Preise von einer Preisliste ab, die sich viele Kunden teilen, kann die Kennung der Preisliste in den Cache-Schlüssel aufgenommen werden, sodass jede Gruppe ihre eigene Kopie hat. Sicher ist das nur, wenn die Kennung aus der geprüften Session stammt. Ein einfaches Cookie wie price_group=3, das der Besucher bearbeiten kann, erlaubt jedem, die gecachte Seite einer anderen Gruppe abzurufen.

Eine einfachere Grenze ist ebenfalls zulässig: den gemeinsamen Cache bei jeder Anfrage mit Session-Cookie umgehen. Das funktioniert, wenn angemeldete Kunden nur einen kleinen Teil der Zugriffe ausmachen. Bei einem stark genutzten B2B-Bestellsystem, in dem fast jeder Besucher angemeldet ist, kommt es stärker auf den geteilten Entwurf an.

Kundenindividuelle Preise cachen: eine Richtlinie für jeden Seitentyp

Seitentypen und die passende Caching-Richtlinie
Seitentyp Persönlicher Inhalt Gemeinsamer Cache Cache-Control in der Antwort
Versionierte CSS-, JavaScript- und Bilddateien Keiner Ja public, max-age=31536000, immutable
Inhalts- und Katalogseiten ohne persönliche Preise Keiner Ja, bei Änderung leeren public, s-maxage gesetzt, kurzes max-age
Gerüst der Produktseite, Preis wird getrennt geladen Keiner im Gerüst Ja public, s-maxage gesetzt, kurzes max-age
Anfrage für Kundenpreis oder Warenkorbanzahl Preis, Warenkorb Nein private, no-store
Produktseite, mit dem Preis des Kunden erzeugt Preis Nein private, no-store
Warenkorb, Kasse, Konto, Bestellungen, Rechnungen Alles Nein no-store
Anmeldung, Abmeldung, jede Antwort, die ein Session-Cookie setzt Session Nein no-store

Mit zwei Kunden und einem anonymen Besucher testen

Dieses Leck zeigt sich nicht, wenn ein Entwickler mit einem Konto testet. Der Test braucht zwei Kunden mit unterschiedlichen Preisen, und er muss durch dieselbe Cache-Kette laufen wie der Produktivbetrieb, nicht direkt zur Anwendung.

  1. Wählen Sie ein Produkt, für das Kunde A und Kunde B unterschiedliche Preise haben. Leeren Sie den Cache für dessen URL oder warten Sie, bis er abgelaufen ist.
  2. Rufen Sie die Seite als Kunde A auf. Halten Sie den angezeigten Preis, etwaige Kontodaten und die Antwort-Header fest.
  3. Rufen Sie dieselbe URL als Kunde B aus einem getrennten Browser-Profil oder HTTP-Client auf. Rufen Sie sie danach ganz ohne Cookies auf.
  4. Vergleichen Sie die drei Antworten. B muss den Preis von B sehen, und der anonyme Besucher darf keinen Kundenpreis, keinen Kontonamen und kein fremdes Session-Cookie sehen.
  5. Lesen Sie die Header: Cache-Control, Vary, Set-Cookie und Age. Ein Age-Header bei einer persönlichen Antwort bedeutet, dass sie aus einem Cache kam. Viele CDNs und Proxys fügen außerdem einen Header hinzu, der einen Treffer oder Fehltreffer meldet.
  6. Wiederholen Sie das Ganze in anderer Reihenfolge: erst anonym, dann A, dann B. Wer zuerst anfragt, füllt den Cache, also prüft jede Reihenfolge einen anderen Fehler.
  7. Führen Sie dieselbe Abfolge für die getrennten Preis- und Warenkorb-Anfragen durch, melden Sie sich dann ab und nutzen Sie auf Kontoseiten die Zurück-Schaltfläche, um den Browser-Cache zu prüfen.

Nehmen Sie die Abfolge in die automatisierten Tests auf, die vor jedem Release laufen, und führen Sie sie nach jeder Änderung an CDN-, Proxy- oder Cache-Einstellungen erneut aus. Eine Verbesserung der Performance ist der wahrscheinlichste Moment, in dem dieser Fehler entsteht, und Kundenportale brauchen denselben Test wie Shops.

Fragen, die wir oft hören

Es hält gemeinsame Caches, die sich korrekt verhalten, davon ab, die Seite zu speichern, aber der Browser darf sie weiterhin behalten. Für Preise und Kontodaten ist private, no-store die sicherere Wahl, und Sie müssen trotzdem prüfen, dass keine CDN- oder Proxy-Regel den Header übersteuert.

Ja. Legen Sie die Teile, die für alle gleich sind, in den gemeinsamen Cache und laden Sie die persönlichen Teile getrennt. Statische Dateien und das Seitengerüst kommen dann aus dem Cache, und nur kleine persönliche Anfragen erreichen die Anwendung.

Ja. Ein Reverse Proxy vor dem Webserver, ein Seiten-Cache-Modul im Server oder ein Caching-Plugin in der Anwendung sind alle gemeinsame Caches und können die Seite eines Kunden an einen anderen ausliefern.

Möchten Sie das erledigt haben, ohne es selbst zu tun?

Erzählen Sie uns, woran Sie arbeiten. Innerhalb eines Werktags antwortet Ihnen ein Mensch.