CVE-2026-41949
Description
Dify version 1.14.1 and prior contain an authorization bypass vulnerability in the file preview endpoint that allows any authenticated user to read up to 3,000 characters of any uploaded document across all tenants and workspaces using only the file's UUID. Attackers can access the /console/api/files/{file_id}/preview endpoint with an intercepted file UUID to extract sensitive content from documents without ownership or workspace permission verification. NOTE: Dify Cloud allows unauthenticated free self-registration, making account creation trivially accessible to any attacker.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
An authorization bypass in Dify ≤1.14.1 lets any authenticated user read up to 3,000 characters of any uploaded document via the file preview endpoint, exposing cross-tenant sensitive data.
Vulnerability
Dify versions 1.14.1 and prior contain an authorization bypass in the file preview endpoint (/console/api/files/{file_id}/preview). The endpoint fails to verify ownership or workspace/tenant permissions, allowing any authenticated user to retrieve the first 3,000 characters of any uploaded document by supplying only the file's UUID. The vulnerability affects all Dify installations including Dify Cloud, where self-registration is freely available [1][2][3].
Exploitation
An attacker needs only a valid authenticated session on a Dify instance (trivially obtained on Dify Cloud via free self-registration) and the UUID of a target file. The UUID may be intercepted from network traffic, API logs, or through other leakage. With the UUID, the attacker sends a GET request to /console/api/files/{file_id}/preview to extract up to 3,000 characters of the document's content without any additional permissions [2][3].
Impact
Successful exploitation results in unauthorized disclosure of sensitive content from uploaded documents across all tenants and workspaces. An attacker can read the first 3,000 characters of any file, potentially exposing credentials, proprietary business data, personal information, or other confidential material. The impact is primarily confidentiality loss, with medium severity (CVSS v3 5.9) [2][3].
Mitigation
The fix was merged in pull request #35797 on May 14, 2026, which adds tenant-scoped authorization checks via current_account_with_tenant() in FilePreviewApi.get [1]. The patch is included in a commit referenced from that PR. Users should upgrade to Dify version 1.14.2 or later as soon as it is released. Until an upgrade is applied, no workaround is documented in the available references [1][2].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2(expand)+ 1 more
- (no CPE)
- (no CPE)range: <=1.14.1
Patches
1432a6412a3fdfix(security): tenant-scope FilePreviewApi text-extract endpoint (GHSA-2qwc-c2cc-2xwv) (#35797)
5 files changed · +14 −11
api/controllers/console/files.py+2 −1 modified@@ -105,7 +105,8 @@ class FilePreviewApi(Resource): @account_initialization_required def get(self, file_id): file_id = str(file_id) - text = FileService(db.engine).get_file_preview(file_id) + _, tenant_id = current_account_with_tenant() + text = FileService(db.engine).get_file_preview(file_id, tenant_id) return {"content": text}
api/services/file_service.py+4 −2 modified@@ -172,12 +172,14 @@ def upload_text(self, text: str, text_name: str, user_id: str, tenant_id: str) - return upload_file - def get_file_preview(self, file_id: str): + def get_file_preview(self, file_id: str, tenant_id: str): """ Return a short text preview extracted from a document file. """ with self._session_maker(expire_on_commit=False) as session: - upload_file = session.scalar(select(UploadFile).where(UploadFile.id == file_id).limit(1)) + upload_file = session.scalar( + select(UploadFile).where(UploadFile.id == file_id, UploadFile.tenant_id == tenant_id).limit(1) + ) if not upload_file: raise NotFound("File not found")
api/tests/test_containers_integration_tests/services/test_file_service.py+4 −4 modified@@ -514,7 +514,7 @@ def test_get_file_preview_success( db_session_with_containers.commit() - result = FileService(engine).get_file_preview(file_id=upload_file.id) + result = FileService(engine).get_file_preview(file_id=upload_file.id, tenant_id=upload_file.tenant_id) assert result == "extracted text content" mock_external_service_dependencies["extract_processor"].load_from_upload_file.assert_called_once() @@ -529,7 +529,7 @@ def test_get_file_preview_file_not_found( non_existent_id = str(fake.uuid4()) with pytest.raises(NotFound, match="File not found"): - FileService(engine).get_file_preview(file_id=non_existent_id) + FileService(engine).get_file_preview(file_id=non_existent_id, tenant_id=str(fake.uuid4())) def test_get_file_preview_unsupported_file_type( self, db_session_with_containers: Session, engine, mock_external_service_dependencies @@ -549,7 +549,7 @@ def test_get_file_preview_unsupported_file_type( db_session_with_containers.commit() with pytest.raises(UnsupportedFileTypeError): - FileService(engine).get_file_preview(file_id=upload_file.id) + FileService(engine).get_file_preview(file_id=upload_file.id, tenant_id=upload_file.tenant_id) def test_get_file_preview_text_truncation( self, db_session_with_containers: Session, engine, mock_external_service_dependencies @@ -572,7 +572,7 @@ def test_get_file_preview_text_truncation( long_text = "x" * 5000 # Longer than PREVIEW_WORDS_LIMIT mock_external_service_dependencies["extract_processor"].load_from_upload_file.return_value = long_text - result = FileService(engine).get_file_preview(file_id=upload_file.id) + result = FileService(engine).get_file_preview(file_id=upload_file.id, tenant_id=upload_file.tenant_id) assert len(result) == 3000 # PREVIEW_WORDS_LIMIT assert result == "x" * 3000
api/tests/unit_tests/controllers/console/test_files.py+1 −1 modified@@ -278,7 +278,7 @@ def test_blocked_extension(self, app, mock_account_context, mock_file_service): class TestFilePreviewApi: - def test_get_preview(self, app, mock_file_service): + def test_get_preview(self, app, mock_account_context, mock_file_service): api = FilePreviewApi() get_method = unwrap(api.get) mock_file_service.get_file_preview.return_value = "preview text"
api/tests/unit_tests/services/test_file_service.py+3 −3 modified@@ -221,23 +221,23 @@ def test_get_file_preview_success(self, file_service, mock_db_session): mock_extract.return_value = "Extracted text content" # Execute - result = file_service.get_file_preview("file_id") + result = file_service.get_file_preview("file_id", "tenant_id") # Assert assert result == "Extracted text content" def test_get_file_preview_not_found(self, file_service, mock_db_session): mock_db_session.scalar.return_value = None with pytest.raises(NotFound, match="File not found"): - file_service.get_file_preview("non_existent") + file_service.get_file_preview("non_existent", "tenant_id") def test_get_file_preview_unsupported_type(self, file_service, mock_db_session): upload_file = MagicMock(spec=UploadFile) upload_file.id = "file_id" upload_file.extension = "exe" mock_db_session.scalar.return_value = upload_file with pytest.raises(UnsupportedFileTypeError): - file_service.get_file_preview("file_id") + file_service.get_file_preview("file_id", "tenant_id") def test_get_image_preview_success(self, file_service, mock_db_session): # Setup
Vulnerability mechanics
Root cause
"Missing tenant-scoped authorization check in the file preview endpoint allows any authenticated user to access any uploaded document by supplying only a file UUID."
Attack vector
An attacker who is authenticated to any Dify tenant (including via free self-registration on Dify Cloud) can call the `/console/api/files/{file_id}/preview` endpoint with an arbitrary file UUID. The endpoint previously queried the `UploadFile` table solely by `file_id` without verifying that the requesting user belongs to the file's tenant [CWE-639]. By enumerating or intercepting file UUIDs, the attacker can read up to 3,000 characters of text content from any uploaded document across all tenants and workspaces. No special privileges or workspace membership are required beyond a valid session.
Affected code
The vulnerability exists in `api/controllers/console/files.py` in the `FilePreviewApi.get()` method and in `api/services/file_service.py` in the `FileService.get_file_preview()` method. The controller called `get_file_preview(file_id)` without providing a tenant context, and the service method queried `UploadFile` only by `file_id` with no tenant filter.
What the fix does
The patch adds a `tenant_id` parameter to `FileService.get_file_preview()` and passes the current account's tenant ID (obtained via `current_account_with_tenant()`) from the `FilePreviewApi` controller [patch_id=424434]. The database query is updated to filter on both `UploadFile.id == file_id` AND `UploadFile.tenant_id == tenant_id`, ensuring that a user can only retrieve a file preview if the file belongs to their own tenant. This closes the authorization bypass by enforcing tenant-scoped access control at the query level.
Preconditions
- authAttacker must be authenticated to any Dify tenant (Dify Cloud allows free self-registration).
- inputAttacker must know or guess a valid file UUID belonging to any tenant.
Generated on May 19, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3- github.com/langgenius/dify/pull/35797nvdIssue TrackingPatchMitigation
- huntr.com/bounties/d50a0240-7951-4939-b989-9bded66c7682nvdExploitThird Party Advisory
- www.vulncheck.com/advisories/dify-authorization-bypass-via-file-preview-endpointnvdThird Party Advisory
News mentions
0No linked articles in our index yet.