Skip to content
Helpdesk & Ticketing

A Bug Report That Didn't Quite Reproduce, and Why We Fixed It Anyway

A researcher reported a stored-XSS path through PDF attachments in FanDesk tickets. The exact path likely didn't reproduce as described; we hardened the code anyway.

H
Hamze Zare Nasiri
September 16, 2026
A Bug Report That Didn't Quite Reproduce, and Why We Fixed It Anyway

A researcher named Dhruv emailed us to report a stored XSS path: someone attaches a PDF with embedded JavaScript to a FanDesk support ticket, and when a support agent opens it, the script runs "in the context of the application."

What we found when we checked

The download route in FanDesk has forced Content-Disposition: attachment and application/octet-stream since December 2025, we checked with git blame. The frontend attachment viewer has zero iframe, embed, or object anywhere in the code; every non-image attachment goes through a Blob download, never an inline render. Uploads are already validated with python-magic, reading the real file bytes instead of trusting the browser's claimed content type, and image/svg+xml is explicitly excluded from the allow-list for exactly this reason, an SVG can carry a <script> tag.

So the exact path Dhruv described likely doesn't reproduce as written. That's still not a reason to leave the code as it was.

What we changed anyway

Upload-time validation only tells you what a file was when someone uploaded it. Nothing stops the stored bytes from being served differently later if some other code path changes. We closed that gap by re-detecting the real MIME type at download time too, from the same bytes on disk, not the type stored in the database:

detected_type = magic.from_file(str(file_path), mime=True)
is_image = FileStorageService.renders_inline_safely(detected_type)

Only an allow-listed image type is ever served as inline; every PDF, and everything we don't explicitly recognize, is forced to download, now with X-Content-Type-Options: nosniff and a Content-Security-Policy: default-src 'none'; sandbox on the response, so a browser can't decide otherwise even if some future code path opens the file directly. We added four tests around this logic and shipped it to production the same day.

The actual lesson

A bug report that doesn't reproduce exactly as written is still worth reading carefully. Dhruv's email pointed at a real gap, not an exploitable one today, but a gap between what we validate at upload and what we trust at download. Closing that gap cost an afternoon. Finding out we needed to cost someone else's careful attention, and earned a reply thanking them for it. A malicious MCP server plays on a similar gap, behaving for the first few calls and then changing what it tells the agent, so we audited the MCP servers in our own AI setup the same way.

PDFs are now also the main carrier for QR code phishing; see what Microsoft's 2026 Digital Defense Report found about QR codes in PDFs.

Helpdesk bugs are not a theoretical risk. In September 2026 two Zammad zero-days took DIVD from its public helpdesk to root; we wrote up what happened and a checklist for self-hosted ticketing.

Frequently asked questions

Can a PDF attachment cause stored XSS in a help desk?

It can if the application renders the PDF inline in its own origin, because a PDF can carry embedded JavaScript. The safe pattern is to force every PDF and unknown file type to download as an attachment, send X-Content-Type-Options: nosniff and a Content-Security-Policy of default-src 'none'; sandbox, and render inline only allow-listed image types.

Why check a file's MIME type at download time, not only at upload?

Upload validation only tells you what a file was when it arrived. If another code path changes later, stored bytes could be served differently. Re-detecting the real type from the bytes on disk at download time, instead of trusting the type saved in the database, closes the gap between what you validate and what you serve.

Should SVG uploads be allowed as images?

Treat them with care. An SVG file can carry a script tag, so serving one inline can run code in your application's origin. FanDesk explicitly excludes image/svg+xml from the inline allow-list, and validates uploads with python-magic, which reads the real file bytes instead of trusting the browser's claimed content type.

Need a Helpdesk That Takes Security Seriously?

FanDesk is enterprise helpdesk software with native RTL Persian, business-hour SLAs, and a self-hosted option — built and hardened by the same team running it in production.

View FanDesk

Share This Article