GitHub Advisoryadvisory · npm
GHSA-x3hf-7cj6-3r4m Flowise RCE via SQLite Record Manager Node
CRITICAL
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
- https://github.com/FlowiseAI/Flowise/security/advisories/GHSA-x3hf-7cj6-3r4m web
- https://github.com/FlowiseAI/Flowise/pull/6464 web
- https://github.com/FlowiseAI/Flowise/commit/d07186844263bad057008863037466aff7c3390f web
- https://github.com/FlowiseAI/Flowise package
- https://github.com/FlowiseAI/Flowise/releases/tag/[email protected] web