Zammad: Stored XSS im Autocomplete der Desktop-Oberfläche

Ein anonym registrierter Name wird im Autocomplete der neuen Desktop-Oberfläche als HTML gerendert und führt JavaScript in der Sitzung des suchenden Agenten aus.

Advisory-ID: TP-2026-116
Produkt: Zammad (webbasiertes Helpdesk- und Ticketsystem)
Schwachstellentyp: Stored Cross-Site Scripting (CWE-79)
CVE: nicht beantragt
CVSS 4.0: 8.6 (Hoch) · CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Betroffene Versionen: <= 7.2.0
Hersteller-Advisory: GHSA-852x-r9wm-4v5j
Behoben in: 7.2.1
Gemeldet: 23. August 2026

Zusammenfassung

Zammad ist ein webbasiertes Helpdesk-System mit einer neuen, auf Vue basierenden Desktop-Oberfläche unter /desktop. Deren Autocomplete-Felder, etwa die Kundensuche beim Anlegen eines Tickets, rendern das Label eines Treffers als HTML und maskieren es nur, wenn das Label den eingegebenen Suchbegriff enthält. Die Suche trifft aber auch über E-Mail-Adresse oder Organisation, während das Label der volle Name ist, sodass bei der üblichen Suche nach einer E-Mail-Adresse das rohe Label im DOM landet. Den Namen setzt jeder, der sich über die öffentliche Registrierung ein Konto anlegt, auch ohne die E-Mail-Bestätigung abzuschließen. Sucht ein Agent in der Desktop-Oberfläche nach diesem Datensatz, läuft fremder Code in seiner Sitzung.

Ursache

CommonSelectItem gibt matchedLabel per v-html aus und unterdrückt dafür die zugehörige Lint-Regel (app/frontend/apps/desktop/components/CommonSelect/CommonSelectItem.vue:101, :110). CommonSelect wendet escape() nur innerhalb von if (option.match && option.match[0]) an und gibt das Label sonst unverändert als matchedLabel weiter (app/frontend/apps/desktop/components/CommonSelect/CommonSelect.vue:349-370). FieldAutoCompleteInput setzt match über einen regulären Ausdruck auf das Label, filtert Treffer ohne Übereinstimmung aber nicht heraus (app/frontend/apps/desktop/components/Form/fields/FieldAutoComplete/FieldAutoCompleteInput.vue:336-341), anders als der Zwilling FieldSelectInput.vue:79 mit .filter((option) => option.match). Der Server liefert als Label den vollen Namen (app/graphql/gql/queries/autocomplete_search/user.rb:53-54), während die Suche auch E-Mail, Login und Organisation abgleicht, sodass match im Normalbetrieb null ist. Die anonyme Registrierung übernimmt firstname und lastname (app/controllers/users_controller.rb:894), und User bereinigt nur das Feld note von HTML (app/models/user.rb:141).

Proof of Concept

POST /api/v1/users                    (anonym, mit csrf-token aus GET /)
{"signup": true, "email": "[email protected]", "password": "<passwort>",
 "firstname": "<iframe srcdoc=\"&lt;script src=/api/v1/ticket_attachment/T/A/X&gt;&lt;/script&gt;\"></iframe>",
 "lastname": "Q"}

-> Agent öffnet /desktop/tickets/create und sucht im Kundenfeld nach "angreifer.example"

Der Suchbegriff kommt im Label nicht vor, deshalb bleibt match leer und das Label wird ungefiltert per v-html eingefügt. Das srcdoc-Dokument erbt Herkunft und CSP der Seite und lädt ein gleich-herkünftiges Skript, etwa einen als application/javascript ausgelieferten Ticket-Anhang, der über window.top die Sitzung des Agenten steuert.

Auswirkung

  • Lesen aller Tickets und internen Notizen, auf die der suchende Agent Zugriff hat.
  • Schreiben von Artikeln und Ticketänderungen im Namen des Agenten.
  • Volle Administrationsrechte, wenn der Agent zugleich Administrator ist.
  • Betroffen ist nur die Vue-Desktop-Oberfläche unter /desktop, die standardmäßig nicht als Hauptoberfläche aktiviert ist (ui_desktop_beta_switch), aber für jeden Agenten erreichbar ist; die klassische und die mobile Oberfläche sind nicht betroffen.

Referenzen

Steckt so etwas in Ihrer Software?

Diese Schwachstelle hat unser Team im Rahmen seiner Arbeit gefunden. Lassen Sie Ihre Anwendungen von denselben Spezialisten prüfen, mit einem Penetrationstest von turingpoint.