Executive Summary

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. 

Introduction

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: 

  1. A file handling flaw that enables one-click account takeover through a single malicious link (detailed below). 
  1. An insufficient tenant isolation issue in shared environments that exposes other users’ application source code.  

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.  

One Click to Account Takeover

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: 

  • upload.dify.ai: where user-uploaded files are stored and served 
  • cloud.dify.aithe main application domain, where users authenticate and manage their workflows 

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:  

  • The threat actor generates a malicious link leading to a resource in his account 
  • The resource link is shared to another user, and one click leads to account takeover. 

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. 

Cross-Tenant Source Disclosure in the Python Sandbox

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. 

  1. Sandboxed Python executions shared a filesystem location. 
  2. Those executions shared the same runtime identity. 
  3. The leaked artifact contained encrypted code, not plaintext. 
  4. But the “encryption” was repeating-key XOR, so ciphertext alone was often enough. 

Where the Leak Came From 

dify1

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: 

  • Identity was fixed through static.SANDBOX_USER_UID. 
  • The wrapper script was written with os.WriteFile(…, 0755). 
  • The file lived under the shared sandbox tmp directory. 

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: 

dify2

What the Attacker Actually Stole

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: 

dify3

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

dify4

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

Something like that: 

dify5

Why the Encryption Broke

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: 

  • generated a random 64-byte key 
  • XORed every plaintext byte with key[i mod 64] 
  • Base64-encoded the result 
  • embedded the ciphertext in the wrapper script 

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: 

  1. Lock onto the real key size of 64 bytes. 
  1. Score candidate plaintext bytes for “Python-likeness.” 
  1. Slide common cribs, import , from , def main( — across the ciphertext. 
  1. Reward outputs that decode as UTF-8, contain Python tokens, and successfully parse with ast.parse. 

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:

dify6

The real PoC is more careful, including crib dragging, UTF-8 heuristics, Python-token scoring, AST validation, and more. 

Why This Was Recoverable in Practice

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. 

How Dify Fixed It

The fix landed in dify-sandbox 0.2.13: 

The patched runner changed the trust boundary in the right place: 

The important changes: 

  • uid, err := AcquireUID(ctx) 
  • The wrapper was written with os.WriteFile(…, 0600). 
  • The file was reassigned with syscall.Chown(…, uid, …). 
  • The embedded prescript stopped using the single global sandbox UID and used the per-run UID instead. 

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. 

The Impact

  • One-click account takeover: The attacker acts as the victim: modifying workflows, changing settings, inviting collaborators. 
  • Workflow theft: Private workflows (often encoding proprietary business logic, integration architecture, and prompt engineering) become fully accessible. 
  • Credential exfiltration: API keys, OAuth tokens, and model configurations stored in Dify can be extracted, enabling lateral movement into every connected external service. 
  • Full instance compromise: If the victim is an administrator, the attacker gains control of the entire Dify deployment and every integration it orchestrates. 

Conclusion

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. 

Timeline

  • January 14, 2026: initial disclosure sent 
  • March 17, 2026: Dify 1.13.1 released, addressing the first issue 
  • March 19, 2026: dify-sandbox 0.2.13 released with UID-based tenant isolation 
  • March 20, 2026: follow-up sandbox patch stabilizes the UID-based design inside the chroot 
  • March 25, 2026: Dify 1.13.3 released, bundling the fixed sandbox at 0.2.14 

The post Dify: When Your AI Platform Becomes the Attack Surface appeared first on Blog.

Oh hi there 👋
It’s nice to meet you.

Sign up to receive awesome content in your inbox, every month.

We don’t spam! Read our privacy policy for more info.

Oh hi there 👋
It’s nice to meet you.

Sign up to receive awesome content in your inbox, every month.

We don’t spam! Read our privacy policy for more info.

By rooter

Leave a Reply