diff --git a/.gitignore b/.gitignore index f3fb37a39f83..eae786eb97e2 100644 --- a/.gitignore +++ b/.gitignore @@ -273,3 +273,5 @@ src/frontend/temp *-shm *-wal .history + +.dspy_cache/ diff --git a/src/backend/base/langflow/api/v1/files.py b/src/backend/base/langflow/api/v1/files.py index b72f00999419..e630a3d4d157 100644 --- a/src/backend/base/langflow/api/v1/files.py +++ b/src/backend/base/langflow/api/v1/files.py @@ -40,6 +40,8 @@ def get_flow_id( async def upload_file( file: UploadFile, flow_id: UUID = Depends(get_flow_id), + current_user=Depends(get_current_active_user), + session=Depends(get_session), storage_service: StorageService = Depends(get_storage_service), ): try: @@ -50,6 +52,10 @@ async def upload_file( ) flow_id_str = str(flow_id) + flow = session.get(Flow, flow_id_str) + if flow.user_id != current_user.id: + raise HTTPException(status_code=403, detail="You don't have access to this flow") + file_content = await file.read() timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") file_name = file.filename or hashlib.sha256(file_content).hexdigest() diff --git a/src/backend/base/langflow/main.py b/src/backend/base/langflow/main.py index 8da3241a38cc..6364b4c5583b 100644 --- a/src/backend/base/langflow/main.py +++ b/src/backend/base/langflow/main.py @@ -1,6 +1,7 @@ import asyncio import json import os +import re import warnings from contextlib import asynccontextmanager from http import HTTPStatus @@ -8,7 +9,7 @@ from urllib.parse import urlencode import nest_asyncio # type: ignore -from fastapi import FastAPI, HTTPException, Request, Response +from fastapi import FastAPI, HTTPException, Request, Response, status from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import FileResponse, JSONResponse from fastapi.staticfiles import StaticFiles @@ -127,6 +128,39 @@ def create_app(): ) app.add_middleware(JavaScriptMIMETypeMiddleware) + @app.middleware("http") + async def check_boundary(request: Request, call_next): + if "/api/v1/files/upload" in request.url.path: + content_type = request.headers.get("Content-Type") + + if not content_type or "multipart/form-data" not in content_type or "boundary=" not in content_type: + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content={"detail": "Content-Type header must be 'multipart/form-data' with a boundary parameter."}, + ) + + boundary = content_type.split("boundary=")[-1].strip() + + if not re.match(r"^[\w\-]{1,70}$", boundary): + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content={"detail": "Invalid boundary format"}, + ) + + body = await request.body() + + boundary_start = f"--{boundary}".encode() + boundary_end = f"--{boundary}--\r\n".encode() + + if not body.startswith(boundary_start) or not body.endswith(boundary_end): + return JSONResponse( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + content={"detail": "Invalid multipart formatting"}, + ) + + response = await call_next(request) + return response + @app.middleware("http") async def flatten_query_string_lists(request: Request, call_next): flattened: list[tuple[str, str]] = [] diff --git a/src/frontend/tests/core/unit/tableInputComponent.spec.ts b/src/frontend/tests/core/unit/tableInputComponent.spec.ts index a52873cd0a44..b2d493bb03a4 100644 --- a/src/frontend/tests/core/unit/tableInputComponent.spec.ts +++ b/src/frontend/tests/core/unit/tableInputComponent.spec.ts @@ -123,7 +123,7 @@ class CustomComponent(Component): ]; for (const text of allVisibleTexts) { - await expect(page.getByText(text)).toBeVisible(); + await expect(page.getByText(text).last()).toBeVisible(); } await page.locator(".ag-cell-value").first().click();