VYPR
High severity7.3NVD Advisory· Published Apr 20, 2026· Updated Apr 29, 2026

CVE-2026-6596

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.

PackageAffected versionsPatched versions
langflow-basePyPI
< 1.9.11.9.1

Patches

1
b5662446bc8c

fix(security): require auth on deprecated /api/v1/upload/{flow_id} (#12831)

https://github.com/langflow-ai/langflowEric HareApr 22, 2026via ghsa
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

News mentions

0

No linked articles in our index yet.