CVE-2026-6596
Description
A security flaw has been discovered in langflow-ai langflow up to 1.1.0. This issue affects the function create_upload_file of the file src/backend/base/Langflow/api/v1/endpoints.py of the component API Endpoint. The manipulation results in unrestricted upload. It is possible to launch the attack remotely. The exploit has been released to the public and may be used for attacks. The vendor was contacted early about this disclosure but did not respond in any way.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
langflow-basePyPI | < 1.9.1 | 1.9.1 |
Patches
1b5662446bc8cfix(security): require auth on deprecated /api/v1/upload/{flow_id} (#12831)
4 files changed · +213 −7
src/backend/base/langflow/api/v1/endpoints.py+21 −3 modified@@ -6,7 +6,7 @@ from collections.abc import AsyncGenerator from http import HTTPStatus from typing import TYPE_CHECKING, Annotated -from uuid import UUID, uuid4 +from uuid import uuid4 import orjson import sqlalchemy as sa @@ -34,6 +34,7 @@ from sqlmodel import select from langflow.api.utils import CurrentActiveUser, DbSession, extract_global_variables_from_headers, parse_value +from langflow.api.v1.files import get_flow from langflow.api.v1.schemas import ( ConfigResponse, CustomComponentRequest, @@ -987,14 +988,31 @@ async def get_task_status(_task_id: str) -> TaskStatusResponse: ) async def create_upload_file( file: UploadFile, - flow_id: UUID, + flow: Annotated[Flow, Depends(get_flow)], + settings_service: Annotated[SettingsService, Depends(get_settings_service)], ) -> UploadFileResponse: """Upload a file for a specific flow (Deprecated). This endpoint is deprecated and will be removed in a future version. + Authorization is handled by the ``get_flow`` dependency, which requires an + authenticated user and verifies flow ownership. Mirrors the + ``max_file_size_upload`` guard on the non-deprecated twin at + ``/api/v1/files/upload/{flow_id}`` so authenticated callers can't fill + disk through this route either. """ try: - flow_id_str = str(flow_id) + max_file_size_upload = settings_service.settings.max_file_size_upload + except Exception as exc: + raise HTTPException(status_code=500, detail=str(exc)) from exc + + if file.size is not None and file.size > max_file_size_upload * 1024 * 1024: + raise HTTPException( + status_code=413, + detail=f"File size is larger than the maximum file size {max_file_size_upload}MB.", + ) + + try: + flow_id_str = str(flow.id) file_path = await asyncio.to_thread(save_uploaded_file, file, folder_name=flow_id_str) return UploadFileResponse(
src/backend/tests/unit/api/v1/test_endpoints.py+59 −0 modified@@ -329,3 +329,62 @@ async def test_get_config_mcp_base_url_from_settings(client: AsyncClient, logged result = response.json() assert response.status_code == status.HTTP_200_OK assert result["mcp_base_url"] == "https://langflow.example.com" + + +async def test_deprecated_upload_rejects_unauthenticated(client: AsyncClient, flow): + """Regression: the deprecated /api/v1/upload/{flow_id} must require auth. + + Previously this endpoint accepted uploads without any credentials, letting + anonymous callers write arbitrary files into a flow's cache folder. + """ + response = await client.post( + f"api/v1/upload/{flow.id}", + files={"file": ("test.txt", b"test content")}, + ) + assert response.status_code != status.HTTP_201_CREATED, ( + "Deprecated upload endpoint must reject unauthenticated requests" + ) + assert response.status_code in { + status.HTTP_401_UNAUTHORIZED, + status.HTTP_403_FORBIDDEN, + }, f"Expected 401/403, got {response.status_code}: {response.text}" + + +async def test_deprecated_upload_authenticated_succeeds(client: AsyncClient, logged_in_headers: dict, flow): + """The deprecated endpoint still works for the flow's owner.""" + response = await client.post( + f"api/v1/upload/{flow.id}", + files={"file": ("test.txt", b"test content")}, + headers=logged_in_headers, + ) + assert response.status_code == status.HTTP_201_CREATED, ( + f"Expected 201 for authenticated owner, got {response.status_code}: {response.text}" + ) + body = response.json() + assert body["flowId"] == str(flow.id) + + +async def test_deprecated_upload_enforces_max_file_size( + client: AsyncClient, logged_in_headers: dict, flow, monkeypatch +): + """Regression: the deprecated upload route must honor ``max_file_size_upload``. + + Without this guard, an authenticated user could still fill disk through + this route by uploading arbitrarily large files, bypassing the limit the + non-deprecated twin at /api/v1/files/upload/{flow_id} already enforces. + """ + from langflow.services.deps import get_settings_service + + settings_service = get_settings_service() + monkeypatch.setattr(settings_service.settings, "max_file_size_upload", 1) # 1 MB + oversized = b"x" * (2 * 1024 * 1024) # 2 MB, exceeds the limit + + response = await client.post( + f"api/v1/upload/{flow.id}", + files={"file": ("big.bin", oversized)}, + headers=logged_in_headers, + ) + + assert response.status_code == status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, ( + f"Expected 413 for oversized upload, got {response.status_code}: {response.text}" + )
src/lfx/src/lfx/load/utils.py+23 −4 modified@@ -1,3 +1,4 @@ +import os from pathlib import Path import httpx @@ -7,13 +8,20 @@ class UploadError(Exception): """Raised when an error occurs during the upload process.""" -def upload(file_path: str, host: str, flow_id: str): +def upload(file_path: str, host: str, flow_id: str, api_key: str | None = None): """Upload a file to Langflow and return the file path. + The upload endpoint now requires authentication (see Langflow + PR #12831). Callers must supply an API key via ``api_key`` or by + setting the ``LANGFLOW_API_KEY`` environment variable; otherwise the + server will reject the request with 401/403. + Args: file_path (str): The path to the file to be uploaded. host (str): The host URL of Langflow. flow_id (UUID): The ID of the flow to which the file belongs. + api_key (str | None): API key sent as ``x-api-key``. If None, + falls back to the ``LANGFLOW_API_KEY`` environment variable. Returns: dict: A dictionary containing the file path. @@ -23,8 +31,10 @@ def upload(file_path: str, host: str, flow_id: str): """ try: url = f"{host}/api/v1/upload/{flow_id}" + resolved_api_key = api_key if api_key is not None else os.environ.get("LANGFLOW_API_KEY") + headers = {"x-api-key": resolved_api_key} if resolved_api_key else {} with Path(file_path).open("rb") as file: - response = httpx.post(url, files={"file": file}) + response = httpx.post(url, files={"file": file}, headers=headers) if response.status_code in {httpx.codes.OK, httpx.codes.CREATED}: return response.json() except Exception as e: @@ -35,7 +45,14 @@ def upload(file_path: str, host: str, flow_id: str): raise UploadError(msg) -def upload_file(file_path: str, host: str, flow_id: str, components: list[str], tweaks: dict | None = None): +def upload_file( + file_path: str, + host: str, + flow_id: str, + components: list[str], + tweaks: dict | None = None, + api_key: str | None = None, +): """Upload a file to Langflow and return the file path. Args: @@ -45,6 +62,8 @@ def upload_file(file_path: str, host: str, flow_id: str, components: list[str], flow_id (UUID): The ID of the flow to which the file belongs. components (str): List of component IDs or names that need the file. tweaks (dict): A dictionary of tweaks to be applied to the file. + api_key (str | None): API key forwarded to :func:`upload`. Falls back + to ``LANGFLOW_API_KEY`` if not supplied. Returns: dict: A dictionary containing the file path and any tweaks that were applied. @@ -53,7 +72,7 @@ def upload_file(file_path: str, host: str, flow_id: str, components: list[str], UploadError: If an error occurs during the upload process. """ try: - response = upload(file_path, host, flow_id) + response = upload(file_path, host, flow_id, api_key=api_key) except Exception as e: msg = f"Error uploading file: {e}" raise UploadError(msg) from e
src/lfx/tests/unit/load/test_upload.py+110 −0 added@@ -0,0 +1,110 @@ +"""Regression tests for the SDK upload helper after the auth change. + +After PR #12831, the server's ``/api/v1/upload/{flow_id}`` endpoint requires +authentication. The SDK helpers in :mod:`lfx.load.utils` were updated to +forward an optional ``api_key`` (or ``LANGFLOW_API_KEY`` env var) as the +``x-api-key`` header so existing callers can pass credentials without +rewriting against the non-deprecated ``/api/v1/files/upload/{flow_id}`` +route. +""" + +from unittest.mock import MagicMock, patch + +import httpx +import pytest +from lfx.load.utils import UploadError, upload, upload_file + + +def _ok_response() -> MagicMock: + response = MagicMock() + response.status_code = httpx.codes.CREATED + response.json.return_value = {"file_path": "flow_id/some_file.txt"} + return response + + +def test_upload_sends_api_key_header_when_passed_explicitly(tmp_path): + file_path = tmp_path / "x.txt" + file_path.write_bytes(b"contents") + + with patch("lfx.load.utils.httpx.post", return_value=_ok_response()) as mock_post: + upload(str(file_path), "http://host", "flow-1", api_key="sk-test-123") # pragma: allowlist secret + + assert mock_post.call_count == 1 + _args, kwargs = mock_post.call_args + assert kwargs["headers"] == {"x-api-key": "sk-test-123"} # pragma: allowlist secret + + +def test_upload_falls_back_to_env_var(tmp_path, monkeypatch): + file_path = tmp_path / "x.txt" + file_path.write_bytes(b"contents") + monkeypatch.setenv("LANGFLOW_API_KEY", "sk-from-env") # pragma: allowlist secret + + with patch("lfx.load.utils.httpx.post", return_value=_ok_response()) as mock_post: + upload(str(file_path), "http://host", "flow-1") + + _args, kwargs = mock_post.call_args + assert kwargs["headers"] == {"x-api-key": "sk-from-env"} # pragma: allowlist secret + + +def test_upload_explicit_api_key_overrides_env_var(tmp_path, monkeypatch): + file_path = tmp_path / "x.txt" + file_path.write_bytes(b"contents") + monkeypatch.setenv("LANGFLOW_API_KEY", "sk-from-env") # pragma: allowlist secret + + with patch("lfx.load.utils.httpx.post", return_value=_ok_response()) as mock_post: + upload(str(file_path), "http://host", "flow-1", api_key="sk-explicit") # pragma: allowlist secret + + _args, kwargs = mock_post.call_args + assert kwargs["headers"] == {"x-api-key": "sk-explicit"} # pragma: allowlist secret + + +def test_upload_sends_no_headers_when_no_api_key(tmp_path, monkeypatch): + """Preserve pre-fix wire format for callers who intentionally pass no key. + + The server will now reject the request, but the SDK should not fabricate + a bogus header. An authn failure at the server is the correct signal. + """ + file_path = tmp_path / "x.txt" + file_path.write_bytes(b"contents") + monkeypatch.delenv("LANGFLOW_API_KEY", raising=False) + + with patch("lfx.load.utils.httpx.post", return_value=_ok_response()) as mock_post: + upload(str(file_path), "http://host", "flow-1") + + _args, kwargs = mock_post.call_args + assert kwargs["headers"] == {} + + +def test_upload_file_forwards_api_key(tmp_path): + """``upload_file`` must pass the api_key through to ``upload``.""" + file_path = tmp_path / "x.txt" + file_path.write_bytes(b"contents") + + with patch("lfx.load.utils.upload", return_value={"file_path": "flow/x.txt"}) as mock_upload: + upload_file( + str(file_path), + host="http://host", + flow_id="flow-1", + components=["comp"], + api_key="sk-explicit", # pragma: allowlist secret + ) + + # pragma: allowlist secret + mock_upload.assert_called_once_with( + str(file_path), + "http://host", + "flow-1", + api_key="sk-explicit", # pragma: allowlist secret + ) + + +def test_upload_raises_upload_error_on_auth_failure(tmp_path, monkeypatch): + """A server-side 401 (no auth sent) surfaces as UploadError to the caller.""" + file_path = tmp_path / "x.txt" + file_path.write_bytes(b"contents") + monkeypatch.delenv("LANGFLOW_API_KEY", raising=False) + + response = MagicMock() + response.status_code = httpx.codes.UNAUTHORIZED + with patch("lfx.load.utils.httpx.post", return_value=response), pytest.raises(UploadError): + upload(str(file_path), "http://host", "flow-1")
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
7- github.com/advisories/GHSA-vvfc-fp59-m92gghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-6596ghsaADVISORY
- gist.github.com/chenhouser2025/c2aabfdee41009cfe45d28a9924742a0nvdWEB
- github.com/langflow-ai/langflow/commit/b5662446bc8c54d928e278d3d26ad95b62425815ghsaWEB
- vuldb.com/submit/791919nvdWEB
- vuldb.com/vuln/358231nvdWEB
- vuldb.com/vuln/358231/ctinvdWEB
News mentions
0No linked articles in our index yet.