The third finding from the same March 2026 audit. It’s the lowest-severity of the three, but it’s a good illustration of how a single missing entry in a blocklist becomes a stored attack primitive — and of how defense-in-depth changes the practical impact.
Fixed in 7.0.1 (backported to 6.5.4), assigned CVE-2026-34718 (CVSS v4 5.3, Moderate).
Background
Zammad sanitizes the HTML of ticket articles with Loofah and a custom scrubber, HtmlSanitizer::Scrubber::Wipe. Among other things, the scrubber walks link attributes and strips dangerous URI schemes from href.
Root cause
The scheme blocklist in remove_invalid_links (lib/html_sanitizer/scrubber/wipe.rb) covered only three schemes:
def remove_invalid_links(node)
%w[href style].each do |attribute_name|
next if !node[attribute_name]
href = cleanup_target(node[attribute_name])
next if !href.match?(%r{(javascript|livescript|vbscript):}i) # data: missing
node.delete(attribute_name)
end
end
data: is absent. Because href is allowlisted on <a> tags, an anchor carrying a data:text/html URI passed through sanitization completely unchanged and was stored in the database as-is.
Attack path
The interesting part is the delivery: this needs no authentication.
- An attacker emails a Zammad-monitored mailbox with an
<a href="data:text/html;base64,...">link disguised as an invoice or document. - Zammad ingests the email, runs the sanitizer, and the
data:URI survives. - A support agent opens the ticket and clicks the link.
- The browser navigates to the attacker’s inline HTML document — for example a pixel-perfect fake login page that posts credentials to an attacker-controlled server.
The same payload can also be injected by any authenticated user through the ticket-creation API.
Proof of concept
The payload is an ordinary-looking anchor whose href is a base64 data:text/html document:
<a href="data:text/html;base64,PHNjcmlwdD4uLi48L3NjcmlwdD4=">📄 View Invoice PDF</a>
After Zammad’s sanitizer it is stored unchanged. Control tests confirmed the known schemes were stripped while data: survived:
javascript:alert(1) → stripped
vbscript:msgbox(1) → stripped
data:text/html,... → survives (stored unchanged)
Impact, and the severity story
In my original report I rated this Medium-High (CVSS 3.1, 7.1 — AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:L/A:N), on the assumption that JavaScript inside the data: URI could execute in deployments without a restrictive Content Security Policy.
Zammad rated it Moderate (CVSS v4 5.3) because the GUI’s deployed CSP prevents the script from actually running when such a link is clicked. That’s a fair downgrade — and a clean illustration of defense-in-depth: the sanitizer gap is real, the malicious content is still stored and rendered, but a second layer (the CSP) blocks code execution. The stored-content and phishing/credential-harvesting risk remains, which is why it was still worth fixing.
Fix
A one-character change — add data to the scheme blocklist:
next if !href.match?(%r{(javascript|livescript|vbscript|data):}i)
References
- GHSA-c2cf-9fc7-jhf3 — Zammad security advisory
- CVE-2026-34718 — NVD entry
- Zammad 7.0.1 release
Thanks again to the Zammad security team.