Skip to content

Commit 73b6612

Browse files
JkaviaJanardan S Kaviaautofix-ci[bot]
authored
fix: prevent RCE via data parameter in build_public_tmp endpoint (#12160)
* fix: prevent RCE via data parameter in build_public_tmp endpoint * [autofix.ci] apply automated fixes --------- Co-authored-by: Janardan S Kavia <janardanskavia@Janardans-MacBook-Pro.local> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 1fdbbfa commit 73b6612

2 files changed

Lines changed: 84 additions & 3 deletions

File tree

src/backend/base/langflow/api/v1/chat.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,6 @@ async def build_public_tmp(
583583
background_tasks: LimitVertexBuildBackgroundTasks,
584584
flow_id: uuid.UUID,
585585
inputs: Annotated[InputValueRequest | None, Body(embed=True)] = None,
586-
data: Annotated[FlowDataRequest | None, Body(embed=True)] = None,
587586
files: list[str] | None = None,
588587
stop_component_id: str | None = None,
589588
start_component_id: str | None = None,
@@ -598,10 +597,16 @@ async def build_public_tmp(
598597
This endpoint is specifically for public flows that don't require authentication.
599598
It uses a client_id cookie to create a deterministic flow ID for tracking purposes.
600599
600+
Security Note:
601+
- The 'data' parameter is NOT accepted to prevent flow definition tampering
602+
- Public flows must execute the stored flow definition only
603+
- The flow definition is always loaded from the database
604+
601605
The endpoint:
602606
1. Verifies the requested flow is marked as public in the database
603607
2. Creates a deterministic UUID based on client_id and flow_id
604608
3. Uses the flow owner's permissions to build the flow
609+
4. Always loads the flow definition from the database
605610
606611
Requirements:
607612
- The flow must be marked as PUBLIC in the database
@@ -611,7 +616,6 @@ async def build_public_tmp(
611616
flow_id: UUID of the public flow to build
612617
background_tasks: Background tasks manager
613618
inputs: Optional input values for the flow
614-
data: Optional flow data
615619
files: Optional files to include
616620
stop_component_id: Optional ID of component to stop at
617621
start_component_id: Optional ID of component to start from
@@ -630,11 +634,12 @@ async def build_public_tmp(
630634
owner_user, new_flow_id = await verify_public_flow_and_get_user(flow_id=flow_id, client_id=client_id)
631635

632636
# Start the flow build using the new flow ID
637+
# data is always None for public flows - they load from database only
633638
job_id = await start_flow_build(
634639
flow_id=new_flow_id,
635640
background_tasks=background_tasks,
636641
inputs=inputs,
637-
data=data,
642+
data=None, # Always None - public flows load from database only
638643
files=files,
639644
stop_component_id=stop_component_id,
640645
start_component_id=start_component_id,

src/backend/tests/unit/test_chat_endpoint.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,3 +432,79 @@ async def mock_cancel_flow_build_with_cancelled_error(*_args, **_kwargs):
432432
finally:
433433
# Restore the original function to avoid affecting other tests
434434
monkeypatch.setattr(langflow.api.v1.chat, "cancel_flow_build", original_cancel_flow_build)
435+
436+
437+
@pytest.mark.benchmark
438+
async def test_build_public_tmp_ignores_data_parameter(client, json_memory_chatbot_no_llm, logged_in_headers):
439+
"""Test that build_public_tmp endpoint silently ignores data parameter for security.
440+
441+
Security Test: Verifies that when a user attempts to provide custom flow data
442+
to the public flow endpoint, FastAPI silently ignores the extra parameter and
443+
the endpoint functions normally using the stored flow data from the database.
444+
"""
445+
# Create a flow
446+
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
447+
448+
# Make the flow public
449+
response = await client.patch(
450+
f"api/v1/flows/{flow_id}",
451+
json={"access_type": "PUBLIC"},
452+
headers=logged_in_headers,
453+
)
454+
assert response.status_code == codes.OK
455+
456+
# Create malicious flow data with different structure
457+
malicious_data = {"nodes": [{"id": "malicious", "data": {"type": "CustomComponent"}}], "edges": []}
458+
459+
# Set a client_id cookie
460+
client.cookies.set("client_id", "test-security-client-123")
461+
462+
# Attempt to build with malicious data - FastAPI will silently ignore it
463+
response = await client.post(
464+
f"api/v1/build_public_tmp/{flow_id}/flow",
465+
json={
466+
"inputs": {"session": "test_session"},
467+
"data": malicious_data, # This will be silently ignored by FastAPI
468+
},
469+
headers={"Content-Type": "application/json"},
470+
)
471+
472+
# Verify the request succeeded - the data parameter is simply ignored
473+
assert response.status_code == codes.OK
474+
response_data = response.json()
475+
assert "job_id" in response_data
476+
477+
478+
@pytest.mark.benchmark
479+
async def test_build_public_tmp_without_data_parameter(client, json_memory_chatbot_no_llm, logged_in_headers):
480+
"""Test that build_public_tmp endpoint works without data parameter.
481+
482+
Security Test: Verifies that when no data parameter is provided, the endpoint
483+
works normally and returns a job_id. This proves the data parameter is optional
484+
and the stored flow definition is always used.
485+
"""
486+
# Create a flow
487+
flow_id = await create_flow(client, json_memory_chatbot_no_llm, logged_in_headers)
488+
489+
# Make the flow public
490+
response = await client.patch(
491+
f"api/v1/flows/{flow_id}",
492+
json={"access_type": "PUBLIC"},
493+
headers=logged_in_headers,
494+
)
495+
assert response.status_code == codes.OK
496+
497+
# Set a client_id cookie
498+
client.cookies.set("client_id", "test-no-data-client")
499+
500+
# Build without providing data parameter
501+
response = await client.post(
502+
f"api/v1/build_public_tmp/{flow_id}/flow",
503+
json={"inputs": {"session": "test_session"}},
504+
headers={"Content-Type": "application/json"},
505+
)
506+
507+
# Verify the request succeeded
508+
assert response.status_code == codes.OK
509+
response_data = response.json()
510+
assert "job_id" in response_data

0 commit comments

Comments
 (0)