Zammad: Stored XSS in the Ticket View Through Article Metadata
A customer stores markup in the metadata of their ticket article that executes JavaScript in the agent's session when the ticket is opened.
Advisory ID: TP-2026-115
Product: Zammad (web-based helpdesk and ticketing system)
Vulnerability type: Stored cross-site scripting (CWE-79)
CVE: not requested
CVSS 4.0: 8.5 (High) · CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:P/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N
Affected versions: <= 7.2.0
Vendor advisory: GHSA-fhq9-h46w-68hg
Fixed in: 7.2.1
Reported: 23 August 2026
Summary
Zammad is a web-based helpdesk in which customers open tickets and agents work on them. When an article is created, the server accepts a freely filled preferences field from the customer that is meant to carry only internally generated metadata such as links from the Facebook channel or S/MIME results. The default ticket view renders two of its sub-keys, links and security.type, without HTML escaping. A customer with standard permissions breaks out of an attribute and loads, through an <iframe srcdoc>, a script they uploaded themselves from the Zammad origin, which the Content Security Policy allows. When an agent opens the ticket, foreign code runs with the agent's session and permissions.
Root cause
When an article is created, param_cleanup keeps the preferences field from the request (app/controllers/concerns/creates_ticket_articles.rb:31-32), and the branch for non-agents overwrites only sender_id, type_id and internal and removes origin_by_id, but leaves preferences untouched (:35-45). The article assets deliver every attribute including preferences unchanged to every reader (app/models/ticket/article/assets.rb:47). The ticket view takes article.preferences.links (app/assets/javascripts/app/controllers/article_view/item.coffee:104), and the template emits link.url and link.target raw with <%- %> into a double-quoted attribute (app/assets/javascripts/app/views/ticket_zoom/article_view.jst.eco:174), as well as securityPreferences.type behind a condition the attacker satisfies themselves (:53, :57). The result reaches the DOM through jQuery .html() (item.coffee:218). The CSP script-src 'self' does not stop this, because the same customer uploads a script as an attachment that is served with the client-supplied Content-Type: application/javascript (app/controllers/application_controller/has_download/download_file.rb:40-44).
Proof of Concept
POST /api/v1/ticket_articles (as customer)
{"ticket_id": <own ticket>, "body": "Hello", "type": "note",
"preferences": {"links": [{"name": "x", "target": "_blank",
"url": "x\"><iframe srcdoc=\"<script src=/api/v1/ticket_attachment/T/A/X></script>\"></iframe><b w=\""}]}}
/api/v1/ticket_attachment/T/A/X is an attachment uploaded by the same customer with Content-Type: application/javascript. The srcdoc document inherits the origin and CSP of the Zammad page, so the same-origin script runs and reaches the agent's session through window.top as soon as the agent opens the ticket.
Impact
- Reading every ticket the agent can access, including other groups and internal notes.
- Writing articles, internal notes and ticket changes as the agent.
- Full administrative rights if the affected agent is also an administrator.
- Combined with the anonymous mass assignment from TP-2026-114, placing the payload needs no existing customer account.
References
Is Something Like This in Your Software?
Our team found this vulnerability in the course of its work. Have your applications tested by the same specialists, with a penetration test from turingpoint.
