GitHub Advisoryadvisory · npm

GHSA-x3hf-7cj6-3r4m Flowise RCE via SQLite Record Manager Node

CRITICAL Published 2026-08-04 Modified 2026-08-04 Official page ↗
Aliases
CVE-2026-69259
Ecosystem
npm
Packages
flowise, flowise-components
Severity
Critical
CVSS vector
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
Weaknesses
CWE-94
Reviewed
2026-08-04

Affected packages

• npm flowise: < 3.1.3 → fixed in 3.1.3 • npm flowise-components: < 3.1.3 → fixed in 3.1.3

Description

============================================================================= Security Advisory elttam Topic: Flowise RCE via SQLite Record Manager Node Module: FlowiseAI/Flowise Disclosed: 24-Apr-2026 Credits: Alex Brown Affects: `FlowiseAI/Flowise 3.1.2` I. Background Flowise 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. Flowise allows users to connect to a local SQLite database for record management of Upsert Vector Store operations. II. Problem Description The database path for the "SQLite Record Manager" node could be overridden using the `additionalConfig` input, as demonstrated in the following code snippet. https://github.com/FlowiseAI/Flowise/blob/[email protected]/packages/components/nodes/recordmanager/SQLiteRecordManager/SQLiteRecordManager.ts ```ts class SQLiteRecordManager_RecordManager implements INode { ... async init(nodeData: INodeData, _: string, options: ICommonObject): Promise { const _tableName = nodeData.inputs?.tableName as string const tableName = _tableName ? _tableName : 'upsertion_records' const additionalConfig = nodeData.inputs?.additionalConfig as string const _namespace = nodeData.inputs?.namespace as string const namespace = _namespace ? _namespace : options.chatflowid const cleanup = nodeData.inputs?.cleanup as string const _sourceIdKey = nodeData.inputs?.sourceIdKey as string const sourceIdKey = _sourceIdKey ? _sourceIdKey : 'source' let additionalConfiguration = {} if (additionalConfig) { try { additionalConfiguration = typeof additionalConfig === 'object' ? additionalConfig : JSON.parse(additionalConfig) } catch (exception) { throw new Error('Invalid JSON in the Additional Configuration: ' + exception) } } const database = path.join(process.env.DATABASE_PATH ?? path.join(getUserHome(), '.flowise'), 'database.sqlite') const sqliteOptions = { database, ...additionalConfiguration, type: 'sqlite' } const args = { sqliteOptions, tableName: tableName } const recordManager = new SQLiteRecordManager(namespace, args) ;(recordManager as any).cleanup = cleanup ;(recordManager as any).sourceIdKey = sourceIdKey return recordManager } } ``` The `additionalConfig` input was user controllable. The intended SQLite database path. Keyword argument expansion of the `additionalConfiguration` variable after the `database` variable, which allows overwriting the preceding `database` setting. An 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` Docker image runs as `root`. However, unlike the Flowise RCE via SQL Database Chain Node vulnerability, 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. https://github.com/FlowiseAI/Flowise/blob/[email protected]/packages/components/nodes/recordmanager/SQLiteRecordManager/SQLiteRecordManager.ts ```ts class SQLiteRecordManager implements RecordManagerInterface { ... sanitizeTableName(tableName: string): string { // Trim and normalize case, turn whitespace into underscores tableName = tableName.trim().toLowerCase().replace(/\s+/g, '_') // Validate using a regex (alphanumeric and underscores only) if (!/^[a-zA-Z0-9_]+$/.test(tableName)) { throw new Error('Invalid table name') } return tableName } ... async createSchema(): Promise { const dataSource = await this.getDataSource() try { const queryRunner = dataSource.createQueryRunner() const tableName = this.sanitizeTableName(this.tableName) await queryRunner.manager.query(` CREATE TABLE IF NOT EXISTS "${tableName}" ( uuid TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))), key TEXT NOT NULL, namespace TEXT NOT NULL, updated_at REAL NOT NULL, group_id TEXT, UNIQUE (key, namespace) ); CREATE INDEX IF NOT EXISTS updated_at_index ON "${tableName}" (updated_at); CREATE INDEX IF NOT EXISTS key_index ON "${tableName}" (key); CREATE INDEX IF NOT EXISTS namespace_index ON "${tableName}" (namespace); CREATE INDEX IF NOT EXISTS group_id_index ON "${tableName}" (group_id);`) // Add doc_id column if it doesn't exist (migration for existing tables) const checkColumn = await queryRunner.manager.query( `SELECT COUNT(*) as count FROM pragma_table_info('${tableName}') WHERE name='doc_id';` ) if (checkColumn[0].count === 0) { await queryRunner.manager.query(`ALTER TABLE "${tableName}" ADD COLUMN doc_id TEXT;`) await queryRunner.manager.query(`CREATE INDEX IF NOT EXISTS doc_id_index ON "${tableName}" (doc_id);`) } await queryRunner.release() } catch (e: any) { // This error indicates that the table already exists // Due to asynchronous nature of the code, it is possible that // the table is created between the time we check if it exists // and the time we try to create it. It can be safely ignored. if ('code' in e && e.code === '23505') { return } throw e } finally { await dataSource.destroy() } } ... async update(keys: Array | string[], updateOptions?: UpdateOptions): Promise { if (keys.length === 0) { return } const dataSource = await this.getDataSource() const queryRunner = dataSource.createQueryRunner() const tableName = this.sanitizeTableName(this.tableName) const updatedAt = await this.getTime() const { timeAtLeast, groupIds: _groupIds } = updateOptions ?? {} if (timeAtLeast && updatedAt 0 && typeof keys[0] === 'object' && 'uid' in keys[0] const keyStrings = isNewFormat ? (keys as Array).map((k) => k.uid) : (keys as string[]) const docIds = isNewFormat ? (keys as Array).map((k) => k.docId) : keys.map(() => null) const groupIds = _groupIds ?? keyStrings.map(() => null) if (groupIds.length !== keyStrings.length) { throw new Error(`Number of keys (${keyStrings.length}) does not match number of group_ids (${groupIds.length})`) } const recordsToUpsert = keyStrings.map((key, i) => [key, this.namespace, updatedAt, groupIds[i]…

Related

Alias · CVE (1)

  • CVE-2026-69259 Flowise is a drag & drop user interface to build a customized large language model flow. vulnerability

Weakness · CWE (1)

  • CWE-94 Improper Control of Generation of Code weakness

References