Zammad 7.1.0 contains an authenticated improper authorization vulnerability in the ticket article attachment cloning endpoint.
A customer who can read a ticket, but who is not authorized to view an internal article in that ticket, can call POST /api/v1/ticket_attachment_upload_clone_by_article/:article_id with the internal article_id. The endpoint validates access to the parent ticket, but it does not validate access to the article itself before copying the article's attached files into an attacker-controlled UploadCache.
After the clone operation, the copied file is represented as an UploadCache attachment created by the requesting user and can be downloaded through GET /api/v1/attachments/:id. This bypasses the article-level authorization that correctly blocks direct access to internal article attachments through GET /api/v1/ticket_attachment/:ticket_id/:article_id/:id.
Vulnerability
Root cause
The cloning route is exposed as an authenticated API endpoint (config/routes/ticket.rb:69-70):
The vulnerable controller action loads the article by attacker-supplied id but authorizes only the parent ticket (app/controllers/ticket_articles_controller.rb:153-159):
defticket_attachment_upload_clone_by_articlearticle = Ticket::Article.find(params[:article_id])
authorize!(article.ticket, :show?)
renderjson: {
attachments:article_attachments_clone(article),
}
end
defticket_attachment_upload_clone_by_articlearticle = Ticket::Article.find(params[:article_id])
authorize!(article.ticket, :show?)
renderjson: {
attachments:article_attachments_clone(article),
}
end
defticket_attachment_upload_clone_by_articlearticle = Ticket::Article.find(params[:article_id])
authorize!(article.ticket, :show?)
renderjson: {
attachments:article_attachments_clone(article),
}
end
defticket_attachment_upload_clone_by_articlearticle = Ticket::Article.find(params[:article_id])
authorize!(article.ticket, :show?)
renderjson: {
attachments:article_attachments_clone(article),
}
end
This is insufficient because a customer can be allowed to view the ticket while still being forbidden from viewing internal articles inside that ticket.
Article-level authorization contains the missing internal-article check (app/policies/ticket/article_policy.rb:5-9):
For internal articles, Ticket::ArticlePolicy#show? denies access unless the user has agent read access to the ticket group.
The direct attachment endpoint performs the correct article authorization (app/controllers/ticket_articles_controller.rb:162-187):
authorize!(article, :show?
authorize!(article, :show?
authorize!(article, :show?
authorize!(article, :show?
This means direct download of the internal attachment is correctly blocked, but the clone endpoint omits the same check.
The clone helper copies attached files to an attacker-controlled UploadCache (app/controllers/concerns/clones_ticket_article_attachments.rb:8-12):
defarticle_attachments_clone(article)
raiseExceptions::UnprocessableContent, __("Need 'form_id' to add attachments to new form.") ifparams[:form_id].blank?article.clone_attachments('UploadCache', params[:form_id], only_attached_attachments:true)
end
defarticle_attachments_clone(article)
raiseExceptions::UnprocessableContent, __("Need 'form_id' to add attachments to new form.") ifparams[:form_id].blank?article.clone_attachments('UploadCache', params[:form_id], only_attached_attachments:true)
end
defarticle_attachments_clone(article)
raiseExceptions::UnprocessableContent, __("Need 'form_id' to add attachments to new form.") ifparams[:form_id].blank?article.clone_attachments('UploadCache', params[:form_id], only_attached_attachments:true)
end
defarticle_attachments_clone(article)
raiseExceptions::UnprocessableContent, __("Need 'form_id' to add attachments to new form.") ifparams[:form_id].blank?article.clone_attachments('UploadCache', params[:form_id], only_attached_attachments:true)
end
The attachment cloning implementation creates new Store records under the supplied target object and id (app/models/concerns/can_clone_attachments.rb:26-60):
Source: authenticated customer controls :article_id in POST /api/v1/ticket_attachment_upload_clone_by_article/:article_id and form_id in the JSON body.
Article lookup:Ticket::Article.find(params[:article_id]) loads the internal article.
Insufficient authorization: the controller calls authorize!(article.ticket, :show?), which is true for the ticket customer.
Missing authorization: the controller does not call authorize!(article, :show?), so Ticket::ArticlePolicy#show? is bypassed.
Clone operation:article.clone_attachments('UploadCache', params[:form_id], only_attached_attachments: true) copies attached files from the internal article.
New object boundary: copied files become UploadCacheStore records tied to the supplied form_id.
Download sink: the customer downloads the cloned file through GET /api/v1/attachments/:id.
Impact
An authenticated customer can read attachments from internal ticket articles in tickets they are allowed to view. Internal articles are intended for agent-only notes and may contain sensitive operational details, credentials, customer information, private correspondence, or other non-public support data.
The issue does not require agent permissions. The attacker only needs:
a valid customer account;
read access to the parent ticket;
knowledge or discovery of an internal article_id in that ticket.
Because direct download of the original internal attachment is forbidden while download of the cloned UploadCache copy succeeds, the issue is an authorization bypass at the article boundary.
PoC
Preconditions
Zammad 7.1.0 running locally.
Application reachable at http://localhost:8080.
A customer account that can view ticket 3:
cve-customer@example.test/ CveCustomer123!
cve-customer@example.test/ CveCustomer123!
cve-customer@example.test/ CveCustomer123!
cve-customer@example.test/ CveCustomer123!
Ticket 3 contains an internal article with id 16.
Internal article 16 has an attached file:
Original attachment id:7
Filename:secret.txt
Contents:SECRET-CVE-EVIDENCE:internal attachment content
Original attachment id:7
Filename:secret.txt
Contents:SECRET-CVE-EVIDENCE:internal attachment content
Original attachment id:7
Filename:secret.txt
Contents:SECRET-CVE-EVIDENCE:internal attachment content
Original attachment id:7
Filename:secret.txt
Contents:SECRET-CVE-EVIDENCE:internal attachment content
Step 1 - Confirm the customer can read the parent ticket
Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.
Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.
Fluid Attacks' solutions enable organizations to identify, prioritize, and remediate vulnerabilities in their software throughout the SDLC. Supported by AI, automated tools, and pentesters, Fluid Attacks accelerates companies' risk exposure mitigation and strengthens their cybersecurity posture.