


























We identified a couple of vulnerabilities in AI automation platform Dify resulting in cross-tenant sensitive information disclosure and one-click account takeover. These findings reinforce the pattern we documented in our previous n8n blogpost: even though AI automation platforms are increasingly becoming integration hubs for complex workflows, their security posture still lags behind their rapid evolution and operational importance.
Dify is an open-source platform for building LLM-powered applications: agents, chatbots, and automated workflows. With over 134,000 GitHub stars and over 10 million docker pulls, it has rapidly become one of the most popular tools in the AI application space, offering both self-hosted and managed cloud deployments.
Our research into Dify uncovered two distinct vulnerabilities that illustrate this risk:
Both findings point to the same structural challenge: platforms that centralize trust must also centralize rigor in how they isolate users and handle untrusted input.
The first issue was addressed in Dify 1.13.1. The second was fixed in the sandbox layer by moving from a shared identity to per-execution UIDs, then shipped to Dify users through the newer sandbox image bundled with 1.13.3.
Dify did not respond to any of our disclosure messages and chose to patch silently.
The flaw lies in how Dify handles file uploads through workflow tool nodes, such as Image Downloader or Image Toolbox.
SVG is an XML-based image format that can natively embed JavaScript, via <script> tags or event handlers on SVG elements. When a browser renders an SVG file served from a trusted origin, any embedded script executes with full access to that origin’s session context, including cookies, local storage, and API calls.
Dify uses two subdomains:
Critically, upload.dify.ai and cloud.dify.ai are configured as DNS aliases. From the browser’s perspective, both subdomains resolve to the same origin. This collapses the intended security boundary: a file that should have been confined to a static asset domain is instead rendered with the full privileges of the application domain.
A malicious SVG uploaded to upload.dify.ai could simply be accessed via cloud.dify.ai, and the browser would execute its JavaScript payload as if it were part of the application itself.
But this design wouldn’t be dangerous if access control was enforced on uploaded files. Each uploaded file receives a unique ID and is stored at a predictable path:
https://upload.dify[.]ai/files/tools/<unique-id>/filename.svg
However, these files are publicly accessible with no authentication and no per-user scoping (a.k.a Insecure Direct Object Reference). Anyone who knows the URL can retrieve the file. And that ID is not necessarily secret: it could leak through Referer headers or surface in shared workspace contexts.
Therefore, in this case, the exploitation scenario was straightforward:
Eventually, Dify team fixed this first issue by overwriting the content-type of the HTTP response to “application/octet-stream”, independently from the nature of the file, represented with the args.as_attachment flag version 1.13.1.
This value triggers download instead of rendering.
This bug lived deeper in the stack, inside dify-sandbox, the service Dify uses to execute untrusted code.
The failure here was particularly interesting, as it required a chain to fully leak other users’ source code on the Dify platform.

Fig. 1: Dify cross-tenant source disclosure
The Dify monorepo only pins the sandbox image. At tag 1.13.1, Dify still shipped langgenius/dify-sandbox:0.2.12 in its compose files:
Inside that sandbox version, the Python runner used a fixed sandbox root:
The important detail is what happened during execution. The runner generated a temporary script under ${LIB_PATH}/tmp/<uuid>.py, which became /tmp/<uuid>.py from the Python process’s perspective after chroot. The same runner stamped every wrapper script with a single hard-coded sandbox UID:
Three lines tell the story:
Separate tenants executing inside the same sandbox root, under the same effective identity, with readable code artifacts left in a shared /tmp. That is the entire isolation bug.
Our proof of concept simply sampled /tmp during execution and collected newly created files. In a shared cloud deployment, that exposed wrapper scripts belonging to other tenants running on the same sandbox host.
The attacker-side workflow looked like this:

The leaked file was not the raw user script.
Dify generated a Python wrapper that loaded a native seccomp helper, decoded a Base64 blob, decrypted it, and exec’d the result.
The decryptor lived in the embedded prescript:
The critical line:

On the Go side, the matching encryption logic was just as direct:

This looks like “encryption,” but it is really a byte-wise Vigenere cipher with a 64-byte repeating key.
Something like that:

If Dify had used a modern authenticated cipher and never exposed the key, reading /tmp/<uuid>.py would still have been bad, but it would not immediately reveal source code. Instead, the runner:
Repeating-key XOR leaks structure across every byte position modulo the key length. Once the key length is known, recovery collapses into a set of small single-byte XOR problems, not a modern cryptanalytic challenge.
Our PoC used exactly that property. The attack strategy:
Workflow code is highly structured plaintext: full of repeated syntax, imports, identifiers, indentation, JSON handling, and predictable scaffolding. Even when the exact business logic is unknown, the shape of Python source gives the attacker enough signal to recover key bytes and reconstruct the rest.
The sandbox did not need to leak the key. The ciphertext was enough.
A reduced version of the recovery logic:

The real PoC is more careful, including crib dragging, UTF-8 heuristics, Python-token scoring, AST validation, and more.
Three properties made the attack reliable.
Fixed key size. The vulnerable runner hard-coded key_len := 64, so the PoC did not have to discover a moving target.
Strong plaintext priors. Python source naturally contains ASCII-heavy text, repeated keywords, common import patterns, indentation and punctuation, and valid UTF-8.
Machine-verifiable output. The PoC did not stop at “looks readable.” It strongly preferred candidates that parsed as real Python, turning recovery into a search problem with a sharp scoring function.
The fix landed in dify-sandbox 0.2.13:
The patched runner changed the trust boundary in the right place:
The important changes:
This matters more than any cryptographic tweak. Before the fix, every execution looked like the same sandbox user. After the fix, each execution got its own identity and its own readable artifact set.
Dify did not “fix the encryption.” It fixed the isolation boundary.
Both vulnerabilities we found in Dify stem from the same oversight: security controls that weren’t designed to keep pace with the platform’s feature growth. As these tools add collaboration, file sharing, and multi-tenant environments, each new surface needs to be hardened with the same rigor as the core application.
What makes this particularly relevant for security teams is the open-source model: Dify is widely self-hosted, meaning unpatched instances may persist long after fixes are released. Organizations running Dify (in any configuration) should verify they are on v1.13.1 or later.
Protect your business for 30 days on Imperva.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。