External Control of File Name or Path in Langflow (original) (raw)

Vulnerability Description


Vulnerability Overview

If an arbitrary path is specified in the request body's fs_path, the server serializes the Flow object into JSON and creates/overwrites a file at that path. There is no path restriction, normalization, or allowed directory enforcement, so absolute paths (e.g., /etc/poc.txt) are interpreted as is.

Vulnerable Code

  1. It receives the request body (flow), updates the DB, and then passes it to the file-writing sink.
    @router.post("/", response_model=FlowRead, status_code=201)
    async def create_flow(
    *,
    session: DbSession,
    flow: FlowCreate,
    current_user: CurrentActiveUser,
    ):
    try:
    db_flow = await _new_flow(session=session, flow=flow, user_id=current_user.id)
    await session.commit()
    await session.refresh(db_flow)

| await _save_flow_to_fs(db_flow) |
| |
| except Exception as e: |
@router.post("/", response_model=FlowRead, status_code=201)
async def create_flow(
*,
session: DbSession,
flow: FlowCreate,
current_user: CurrentActiveUser,
):
try:
db_flow = await _new_flow(session=session, flow=flow, user_id=current_user.id)
await session.commit()
await session.refresh(db_flow)
await _save_flow_to_fs(db_flow)
except Exception as e: 2. Applies authentication dependency (requires API Key/JWT) when accessing the endpoint.

CurrentActiveUser = Annotated[User, Depends(get_current_active_user)]
CurrentActiveMCPUser = Annotated[User, Depends(get_current_active_user_mcp)]
DbSession = Annotated[AsyncSession, Depends(get_session)]
CurrentActiveUser = Annotated[User, Depends(get_current_active_user)]
CurrentActiveMCPUser = Annotated[User, Depends(get_current_active_user_mcp)]
DbSession = Annotated[AsyncSession, Depends(get_session)]
3. The client can directly specify the save path, including fs_path.
):
---------------------------------------
try:
await _verify_fs_path(flow.fs_path)
"""Create a new flow."""
):
try:  
    await _verify_fs_path(flow.fs_path)  
    """Create a new flow."""
  1. It attempts to create the file (or the file, in the case of a path without a parent) directly without path validation.
    async def _verify_fs_path(path: str | None) -> None:
    if path:
    path_ = Path(path)
    if not await path_.exists():
    await path_.touch()
    async def _verify_fs_path(path: str
    if path:
     path_ = Path(path)  
     if not await path_.exists():  
         await path_.touch()
  2. Serializes the Flow object to JSON and writes it to the specified path in "w" mode (overwriting).
    async def _save_flow_to_fs(flow: Flow) -> None:
    if flow.fs_path:
    async with async_open(flow.fs_path, "w") as f:
    try:
    await f.write(flow.model_dump_json())
    except OSError:
    await logger.aexception("Failed to write flow %s to path %s", flow.name, flow.fs_path)
    async def _save_flow_to_fs(flow: Flow) -> None:
    if flow.fs_path:
     async with async_open(flow.fs_path, "w") as f:  
         try:  
             await f.write(flow.model_dump_json())  
         except OSError:  
             await logger.aexception("Failed to write flow %s to path %s", flow.name, flow.fs_path)

PoC


PoC Description

When an authenticated user passes an arbitrary path in fs_path, the Flow JSON is written to that path. Since /tmp is usually writable, it is easy to reproduce. In a production environment, writing to system-protected directories may fail depending on permissions.

PoC

Impact