CVE-2025-29189
Description
Flowise <= 2.2.3 is vulnerable to SQL Injection. via tableName parameter at Postgres_VectorStores.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Flowise <= 2.2.3 is vulnerable to SQL injection via the tableName parameter in Postgres_VectorStores, allowing attackers to manipulate database queries.
Vulnerability
Overview CVE-2025-29189 affects Flowise versions up to and including 2.2.3. The vulnerability is a SQL injection flaw located in the Postgres_VectorStores component. The root cause is the lack of sanitization of the tableName parameter before it is interpolated into SQL queries [1][2].
Exploitation
Details An attacker can exploit this vulnerability by providing a maliciously crafted table name through the tableName parameter. Since the parameter is directly concatenated into SQL statements without proper validation or parameterization, the attacker can inject arbitrary SQL commands [2]. The attack requires access to the Postgres vector store functionality, which may be exposed through the application's API or user interface.
Impact
Successful exploitation could allow an attacker to read, modify, or delete arbitrary data in the underlying PostgreSQL database. This could lead to unauthorized access to sensitive information, data corruption, or complete compromise of the database. Given the potential for full database manipulation, the impact is severe.
Mitigation
The vulnerability has been patched in commit 9a417bdc95f58d6dd92cbf60dad42414aba34754, which introduces a sanitizeTableName function that validates input against a strict regex (/^[a-zA-Z0-9_]+$/), allowing only alphanumeric characters and underscores [2]. Users are strongly advised to upgrade to a version containing this fix. No workarounds have been documented.
AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
flowise-componentsnpm | < 2.2.4 | 2.2.4 |
Affected products
3- Flowise/Flowisedescription
Patches
19a417bdc95f5Bugfix/update nodevm sandbox options, sanitize tablename (#3818)
16 files changed · +269 −76
packages/components/nodes/documentloaders/CustomDocumentLoader/CustomDocumentLoader.ts+12 −2 modified@@ -106,7 +106,14 @@ class CustomDocumentLoader_DocumentLoaders implements INode { } } - let sandbox: any = { $input: input } + let sandbox: any = { + $input: input, + util: undefined, + Symbol: undefined, + child_process: undefined, + fs: undefined, + process: undefined + } sandbox['$vars'] = prepareSandboxVars(variables) sandbox['$flow'] = flow @@ -128,7 +135,10 @@ class CustomDocumentLoader_DocumentLoaders implements INode { require: { external: { modules: deps }, builtin: builtinDeps - } + }, + eval: false, + wasm: false, + timeout: 10000 } as any const vm = new NodeVM(nodeVMOptions)
packages/components/nodes/memory/AgentMemory/MySQLAgentMemory/mysqlSaver.ts+23 −6 modified@@ -19,6 +19,18 @@ export class MySQLSaver extends BaseCheckpointSaver implements MemoryMethods { this.threadId = threadId } + 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 + } + private async getDataSource(): Promise<DataSource> { const { datasourceOptions } = this.config if (!datasourceOptions) { @@ -38,8 +50,9 @@ export class MySQLSaver extends BaseCheckpointSaver implements MemoryMethods { try { const queryRunner = dataSource.createQueryRunner() + const tableName = this.sanitizeTableName(this.tableName) await queryRunner.manager.query(` - CREATE TABLE IF NOT EXISTS ${this.tableName} ( + CREATE TABLE IF NOT EXISTS ${tableName} ( thread_id VARCHAR(255) NOT NULL, checkpoint_id VARCHAR(255) NOT NULL, parent_id VARCHAR(255), @@ -62,12 +75,13 @@ export class MySQLSaver extends BaseCheckpointSaver implements MemoryMethods { const thread_id = config.configurable?.thread_id || this.threadId const checkpoint_id = config.configurable?.checkpoint_id + const tableName = this.sanitizeTableName(this.tableName) try { const queryRunner = dataSource.createQueryRunner() const sql = checkpoint_id - ? `SELECT checkpoint, parent_id, metadata FROM ${this.tableName} WHERE thread_id = ? AND checkpoint_id = ?` - : `SELECT thread_id, checkpoint_id, parent_id, checkpoint, metadata FROM ${this.tableName} WHERE thread_id = ? ORDER BY checkpoint_id DESC LIMIT 1` + ? `SELECT checkpoint, parent_id, metadata FROM ${tableName} WHERE thread_id = ? AND checkpoint_id = ?` + : `SELECT thread_id, checkpoint_id, parent_id, checkpoint, metadata FROM ${tableName} WHERE thread_id = ? ORDER BY checkpoint_id DESC LIMIT 1` const rows = await queryRunner.manager.query(sql, checkpoint_id ? [thread_id, checkpoint_id] : [thread_id]) await queryRunner.release() @@ -108,7 +122,8 @@ export class MySQLSaver extends BaseCheckpointSaver implements MemoryMethods { const queryRunner = dataSource.createQueryRunner() try { const threadId = config.configurable?.thread_id || this.threadId - let sql = `SELECT thread_id, checkpoint_id, parent_id, checkpoint, metadata FROM ${this.tableName} WHERE thread_id = ? ${ + const tableName = this.sanitizeTableName(this.tableName) + let sql = `SELECT thread_id, checkpoint_id, parent_id, checkpoint, metadata FROM ${tableName} WHERE thread_id = ? ${ before ? 'AND checkpoint_id < ?' : '' } ORDER BY checkpoint_id DESC` if (limit) { @@ -163,8 +178,9 @@ export class MySQLSaver extends BaseCheckpointSaver implements MemoryMethods { Buffer.from(this.serde.stringify(checkpoint)), // Encode to binary Buffer.from(this.serde.stringify(metadata)) // Encode to binary ] + const tableName = this.sanitizeTableName(this.tableName) - const query = `INSERT INTO ${this.tableName} (thread_id, checkpoint_id, parent_id, checkpoint, metadata) + const query = `INSERT INTO ${tableName} (thread_id, checkpoint_id, parent_id, checkpoint, metadata) VALUES (?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE checkpoint = VALUES(checkpoint), metadata = VALUES(metadata)` @@ -190,10 +206,11 @@ export class MySQLSaver extends BaseCheckpointSaver implements MemoryMethods { const dataSource = await this.getDataSource() await this.setup(dataSource) + const tableName = this.sanitizeTableName(this.tableName) try { const queryRunner = dataSource.createQueryRunner() - const query = `DELETE FROM ${this.tableName} WHERE thread_id = ?;` + const query = `DELETE FROM ${tableName} WHERE thread_id = ?;` await queryRunner.manager.query(query, [threadId]) await queryRunner.release() } catch (error) {
packages/components/nodes/memory/AgentMemory/PostgresAgentMemory/pgSaver.ts+29 −12 modified@@ -19,6 +19,18 @@ export class PostgresSaver extends BaseCheckpointSaver implements MemoryMethods this.threadId = threadId } + 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 + } + private async getDataSource(): Promise<DataSource> { const { datasourceOptions } = this.config if (!datasourceOptions) { @@ -40,8 +52,9 @@ export class PostgresSaver extends BaseCheckpointSaver implements MemoryMethods try { const queryRunner = dataSource.createQueryRunner() + const tableName = this.sanitizeTableName(this.tableName) await queryRunner.manager.query(` -CREATE TABLE IF NOT EXISTS ${this.tableName} ( +CREATE TABLE IF NOT EXISTS ${tableName} ( thread_id TEXT NOT NULL, checkpoint_id TEXT NOT NULL, parent_id TEXT, @@ -63,12 +76,13 @@ CREATE TABLE IF NOT EXISTS ${this.tableName} ( const thread_id = config.configurable?.thread_id || this.threadId const checkpoint_id = config.configurable?.checkpoint_id + const tableName = this.sanitizeTableName(this.tableName) if (checkpoint_id) { try { const queryRunner = dataSource.createQueryRunner() const keys = [thread_id, checkpoint_id] - const sql = `SELECT checkpoint, parent_id, metadata FROM ${this.tableName} WHERE thread_id = $1 AND checkpoint_id = $2` + const sql = `SELECT checkpoint, parent_id, metadata FROM ${tableName} WHERE thread_id = $1 AND checkpoint_id = $2` const rows = await queryRunner.manager.query(sql, keys) await queryRunner.release() @@ -89,16 +103,16 @@ CREATE TABLE IF NOT EXISTS ${this.tableName} ( } } } catch (error) { - console.error(`Error retrieving ${this.tableName}`, error) - throw new Error(`Error retrieving ${this.tableName}`) + console.error(`Error retrieving ${tableName}`, error) + throw new Error(`Error retrieving ${tableName}`) } finally { await dataSource.destroy() } } else { try { const queryRunner = dataSource.createQueryRunner() const keys = [thread_id] - const sql = `SELECT thread_id, checkpoint_id, parent_id, checkpoint, metadata FROM ${this.tableName} WHERE thread_id = $1 ORDER BY checkpoint_id DESC LIMIT 1` + const sql = `SELECT thread_id, checkpoint_id, parent_id, checkpoint, metadata FROM ${tableName} WHERE thread_id = $1 ORDER BY checkpoint_id DESC LIMIT 1` const rows = await queryRunner.manager.query(sql, keys) await queryRunner.release() @@ -124,8 +138,8 @@ CREATE TABLE IF NOT EXISTS ${this.tableName} ( } } } catch (error) { - console.error(`Error retrieving ${this.tableName}`, error) - throw new Error(`Error retrieving ${this.tableName}`) + console.error(`Error retrieving ${tableName}`, error) + throw new Error(`Error retrieving ${tableName}`) } finally { await dataSource.destroy() } @@ -139,7 +153,8 @@ CREATE TABLE IF NOT EXISTS ${this.tableName} ( const queryRunner = dataSource.createQueryRunner() const thread_id = config.configurable?.thread_id || this.threadId - let sql = `SELECT thread_id, checkpoint_id, parent_id, checkpoint, metadata FROM ${this.tableName} WHERE thread_id = $1` + const tableName = this.sanitizeTableName(this.tableName) + let sql = `SELECT thread_id, checkpoint_id, parent_id, checkpoint, metadata FROM ${tableName} WHERE thread_id = $1` const args = [thread_id] if (before?.configurable?.checkpoint_id) { @@ -179,8 +194,8 @@ CREATE TABLE IF NOT EXISTS ${this.tableName} ( } } } catch (error) { - console.error(`Error listing ${this.tableName}`, error) - throw new Error(`Error listing ${this.tableName}`) + console.error(`Error listing ${tableName}`, error) + throw new Error(`Error listing ${tableName}`) } finally { await dataSource.destroy() } @@ -200,8 +215,9 @@ CREATE TABLE IF NOT EXISTS ${this.tableName} ( Buffer.from(this.serde.stringify(checkpoint)), // Encode to binary Buffer.from(this.serde.stringify(metadata)) // Encode to binary ] + const tableName = this.sanitizeTableName(this.tableName) - const query = `INSERT INTO ${this.tableName} (thread_id, checkpoint_id, parent_id, checkpoint, metadata) + const query = `INSERT INTO ${tableName} (thread_id, checkpoint_id, parent_id, checkpoint, metadata) VALUES ($1, $2, $3, $4, $5) ON CONFLICT (thread_id, checkpoint_id) DO UPDATE SET checkpoint = EXCLUDED.checkpoint, metadata = EXCLUDED.metadata` @@ -230,8 +246,9 @@ CREATE TABLE IF NOT EXISTS ${this.tableName} ( const dataSource = await this.getDataSource() await this.setup(dataSource) + const tableName = this.sanitizeTableName(this.tableName) - const query = `DELETE FROM "${this.tableName}" WHERE thread_id = $1;` + const query = `DELETE FROM "${tableName}" WHERE thread_id = $1;` try { const queryRunner = dataSource.createQueryRunner()
packages/components/nodes/memory/AgentMemory/SQLiteAgentMemory/sqliteSaver.ts+29 −14 modified@@ -19,6 +19,18 @@ export class SqliteSaver extends BaseCheckpointSaver implements MemoryMethods { this.threadId = threadId } + 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 + } + private async getDataSource(): Promise<DataSource> { const { datasourceOptions } = this.config const dataSource = new DataSource(datasourceOptions) @@ -33,8 +45,9 @@ export class SqliteSaver extends BaseCheckpointSaver implements MemoryMethods { try { const queryRunner = dataSource.createQueryRunner() + const tableName = this.sanitizeTableName(this.tableName) await queryRunner.manager.query(` -CREATE TABLE IF NOT EXISTS ${this.tableName} ( +CREATE TABLE IF NOT EXISTS ${tableName} ( thread_id TEXT NOT NULL, checkpoint_id TEXT NOT NULL, parent_id TEXT, @@ -56,12 +69,13 @@ CREATE TABLE IF NOT EXISTS ${this.tableName} ( const thread_id = config.configurable?.thread_id || this.threadId const checkpoint_id = config.configurable?.checkpoint_id + const tableName = this.sanitizeTableName(this.tableName) if (checkpoint_id) { try { const queryRunner = dataSource.createQueryRunner() const keys = [thread_id, checkpoint_id] - const sql = `SELECT checkpoint, parent_id, metadata FROM ${this.tableName} WHERE thread_id = ? AND checkpoint_id = ?` + const sql = `SELECT checkpoint, parent_id, metadata FROM ${tableName} WHERE thread_id = ? AND checkpoint_id = ?` const rows = await queryRunner.manager.query(sql, [...keys]) await queryRunner.release() @@ -82,16 +96,16 @@ CREATE TABLE IF NOT EXISTS ${this.tableName} ( } } } catch (error) { - console.error(`Error retrieving ${this.tableName}`, error) - throw new Error(`Error retrieving ${this.tableName}`) + console.error(`Error retrieving ${tableName}`, error) + throw new Error(`Error retrieving ${tableName}`) } finally { await dataSource.destroy() } } else { try { const queryRunner = dataSource.createQueryRunner() const keys = [thread_id] - const sql = `SELECT thread_id, checkpoint_id, parent_id, checkpoint, metadata FROM ${this.tableName} WHERE thread_id = ? ORDER BY checkpoint_id DESC LIMIT 1` + const sql = `SELECT thread_id, checkpoint_id, parent_id, checkpoint, metadata FROM ${tableName} WHERE thread_id = ? ORDER BY checkpoint_id DESC LIMIT 1` const rows = await queryRunner.manager.query(sql, [...keys]) await queryRunner.release() @@ -117,8 +131,8 @@ CREATE TABLE IF NOT EXISTS ${this.tableName} ( } } } catch (error) { - console.error(`Error retrieving ${this.tableName}`, error) - throw new Error(`Error retrieving ${this.tableName}`) + console.error(`Error retrieving ${tableName}`, error) + throw new Error(`Error retrieving ${tableName}`) } finally { await dataSource.destroy() } @@ -132,7 +146,8 @@ CREATE TABLE IF NOT EXISTS ${this.tableName} ( const queryRunner = dataSource.createQueryRunner() const thread_id = config.configurable?.thread_id || this.threadId - let sql = `SELECT thread_id, checkpoint_id, parent_id, checkpoint, metadata FROM ${this.tableName} WHERE thread_id = ? ${ + const tableName = this.sanitizeTableName(this.tableName) + let sql = `SELECT thread_id, checkpoint_id, parent_id, checkpoint, metadata FROM ${tableName} WHERE thread_id = ? ${ before ? 'AND checkpoint_id < ?' : '' } ORDER BY checkpoint_id DESC` if (limit) { @@ -167,8 +182,8 @@ CREATE TABLE IF NOT EXISTS ${this.tableName} ( } } } catch (error) { - console.error(`Error listing ${this.tableName}`, error) - throw new Error(`Error listing ${this.tableName}`) + console.error(`Error listing ${tableName}`, error) + throw new Error(`Error listing ${tableName}`) } finally { await dataSource.destroy() } @@ -188,8 +203,8 @@ CREATE TABLE IF NOT EXISTS ${this.tableName} ( this.serde.stringify(checkpoint), this.serde.stringify(metadata) ] - - const query = `INSERT OR REPLACE INTO ${this.tableName} (thread_id, checkpoint_id, parent_id, checkpoint, metadata) VALUES (?, ?, ?, ?, ?)` + const tableName = this.sanitizeTableName(this.tableName) + const query = `INSERT OR REPLACE INTO ${tableName} (thread_id, checkpoint_id, parent_id, checkpoint, metadata) VALUES (?, ?, ?, ?, ?)` await queryRunner.manager.query(query, row) await queryRunner.release() @@ -215,8 +230,8 @@ CREATE TABLE IF NOT EXISTS ${this.tableName} ( const dataSource = await this.getDataSource() await this.setup(dataSource) - - const query = `DELETE FROM "${this.tableName}" WHERE thread_id = ?;` + const tableName = this.sanitizeTableName(this.tableName) + const query = `DELETE FROM "${tableName}" WHERE thread_id = ?;` try { const queryRunner = dataSource.createQueryRunner()
packages/components/nodes/recordmanager/MySQLRecordManager/MySQLrecordManager.ts+24 −7 modified@@ -178,6 +178,18 @@ class MySQLRecordManager implements RecordManagerInterface { this.config = config } + 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 + } + private async getDataSource(): Promise<DataSource> { const { mysqlOptions } = this.config if (!mysqlOptions) { @@ -196,8 +208,9 @@ class MySQLRecordManager implements RecordManagerInterface { try { const dataSource = await this.getDataSource() const queryRunner = dataSource.createQueryRunner() + const tableName = this.sanitizeTableName(this.tableName) - await queryRunner.manager.query(`create table if not exists \`${this.tableName}\` ( + await queryRunner.manager.query(`create table if not exists \`${this.sanitizeTableName(tableName)}\` ( \`uuid\` varchar(36) primary key default (UUID()), \`key\` varchar(255) not null, \`namespace\` varchar(255) not null, @@ -211,11 +224,11 @@ class MySQLRecordManager implements RecordManagerInterface { // MySQL does not support 'IF NOT EXISTS' function for Index const Check = await queryRunner.manager.query( `SELECT COUNT(1) IndexIsThere FROM INFORMATION_SCHEMA.STATISTICS - WHERE table_schema=DATABASE() AND table_name='${this.tableName}' AND index_name='${column}_index';` + WHERE table_schema=DATABASE() AND table_name='${tableName}' AND index_name='${column}_index';` ) if (Check[0].IndexIsThere === 0) await queryRunner.manager.query(`CREATE INDEX \`${column}_index\` - ON \`${this.tableName}\` (\`${column}\`);`) + ON \`${tableName}\` (\`${column}\`);`) } await queryRunner.release() @@ -253,6 +266,7 @@ class MySQLRecordManager implements RecordManagerInterface { 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 ?? {} @@ -275,7 +289,7 @@ class MySQLRecordManager implements RecordManagerInterface { ]) const query = ` - INSERT INTO \`${this.tableName}\` (\`key\`, \`namespace\`, \`updated_at\`, \`group_id\`) + INSERT INTO \`${tableName}\` (\`key\`, \`namespace\`, \`updated_at\`, \`group_id\`) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE \`updated_at\` = VALUES(\`updated_at\`)` @@ -302,12 +316,13 @@ class MySQLRecordManager implements RecordManagerInterface { const dataSource = await this.getDataSource() const queryRunner = dataSource.createQueryRunner() + const tableName = this.sanitizeTableName(this.tableName) // Prepare the placeholders and the query const placeholders = keys.map(() => `?`).join(', ') const query = ` SELECT \`key\` - FROM \`${this.tableName}\` + FROM \`${tableName}\` WHERE \`namespace\` = ? AND \`key\` IN (${placeholders})` // Initialize an array to fill with the existence checks @@ -335,10 +350,11 @@ class MySQLRecordManager implements RecordManagerInterface { async listKeys(options?: ListKeyOptions): Promise<string[]> { const dataSource = await this.getDataSource() const queryRunner = dataSource.createQueryRunner() + const tableName = this.sanitizeTableName(this.tableName) try { const { before, after, limit, groupIds } = options ?? {} - let query = `SELECT \`key\` FROM \`${this.tableName}\` WHERE \`namespace\` = ?` + let query = `SELECT \`key\` FROM \`${tableName}\` WHERE \`namespace\` = ?` const values: (string | number | string[])[] = [this.namespace] if (before) { @@ -385,9 +401,10 @@ class MySQLRecordManager implements RecordManagerInterface { const dataSource = await this.getDataSource() const queryRunner = dataSource.createQueryRunner() + const tableName = this.sanitizeTableName(this.tableName) const placeholders = keys.map(() => '?').join(', ') - const query = `DELETE FROM \`${this.tableName}\` WHERE \`namespace\` = ? AND \`key\` IN (${placeholders});` + const query = `DELETE FROM \`${tableName}\` WHERE \`namespace\` = ? AND \`key\` IN (${placeholders});` const values = [this.namespace, ...keys].map((v) => (typeof v !== 'string' ? `${v}` : v)) // Directly using try/catch with async/await for cleaner flow
packages/components/nodes/recordmanager/PostgresRecordManager/PostgresRecordManager.ts+27 −9 modified@@ -186,6 +186,18 @@ class PostgresRecordManager implements RecordManagerInterface { this.config = config } + 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 + } + private async getDataSource(): Promise<DataSource> { const { postgresConnectionOptions } = this.config if (!postgresConnectionOptions) { @@ -204,20 +216,21 @@ class PostgresRecordManager implements RecordManagerInterface { try { const dataSource = await this.getDataSource() const queryRunner = dataSource.createQueryRunner() + const tableName = this.sanitizeTableName(this.tableName) await queryRunner.manager.query(` - CREATE TABLE IF NOT EXISTS "${this.tableName}" ( + CREATE TABLE IF NOT EXISTS "${tableName}" ( uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(), key TEXT NOT NULL, namespace TEXT NOT NULL, updated_at Double PRECISION NOT NULL, group_id TEXT, UNIQUE (key, namespace) ); - CREATE INDEX IF NOT EXISTS updated_at_index ON "${this.tableName}" (updated_at); - CREATE INDEX IF NOT EXISTS key_index ON "${this.tableName}" (key); - CREATE INDEX IF NOT EXISTS namespace_index ON "${this.tableName}" (namespace); - CREATE INDEX IF NOT EXISTS group_id_index ON "${this.tableName}" (group_id);`) + 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);`) await queryRunner.release() } catch (e: any) { @@ -269,6 +282,7 @@ class PostgresRecordManager implements RecordManagerInterface { 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 ?? {} @@ -287,7 +301,7 @@ class PostgresRecordManager implements RecordManagerInterface { const valuesPlaceholders = recordsToUpsert.map((_, j) => this.generatePlaceholderForRowAt(j, recordsToUpsert[0].length)).join(', ') - const query = `INSERT INTO "${this.tableName}" (key, namespace, updated_at, group_id) VALUES ${valuesPlaceholders} ON CONFLICT (key, namespace) DO UPDATE SET updated_at = EXCLUDED.updated_at;` + const query = `INSERT INTO "${tableName}" (key, namespace, updated_at, group_id) VALUES ${valuesPlaceholders} ON CONFLICT (key, namespace) DO UPDATE SET updated_at = EXCLUDED.updated_at;` try { await queryRunner.manager.query(query, recordsToUpsert.flat()) await queryRunner.release() @@ -306,12 +320,13 @@ class PostgresRecordManager implements RecordManagerInterface { const dataSource = await this.getDataSource() const queryRunner = dataSource.createQueryRunner() + const tableName = this.sanitizeTableName(this.tableName) const startIndex = 2 const arrayPlaceholders = keys.map((_, i) => `$${i + startIndex}`).join(', ') const query = ` - SELECT k, (key is not null) ex from unnest(ARRAY[${arrayPlaceholders}]) k left join "${this.tableName}" on k=key and namespace = $1; + SELECT k, (key is not null) ex from unnest(ARRAY[${arrayPlaceholders}]) k left join "${tableName}" on k=key and namespace = $1; ` try { const res = await queryRunner.manager.query(query, [this.namespace, ...keys.flat()]) @@ -327,7 +342,9 @@ class PostgresRecordManager implements RecordManagerInterface { async listKeys(options?: ListKeyOptions): Promise<string[]> { const { before, after, limit, groupIds } = options ?? {} - let query = `SELECT key FROM "${this.tableName}" WHERE namespace = $1` + const tableName = this.sanitizeTableName(this.tableName) + + let query = `SELECT key FROM "${tableName}" WHERE namespace = $1` const values: (string | number | (string | null)[])[] = [this.namespace] let index = 2 @@ -379,9 +396,10 @@ class PostgresRecordManager implements RecordManagerInterface { const dataSource = await this.getDataSource() const queryRunner = dataSource.createQueryRunner() + const tableName = this.sanitizeTableName(this.tableName) try { - const query = `DELETE FROM "${this.tableName}" WHERE namespace = $1 AND key = ANY($2);` + const query = `DELETE FROM "${tableName}" WHERE namespace = $1 AND key = ANY($2);` await queryRunner.manager.query(query, [this.namespace, keys]) await queryRunner.release() } catch (error) {
packages/components/nodes/recordmanager/SQLiteRecordManager/SQLiteRecordManager.ts+27 −9 modified@@ -156,6 +156,18 @@ class SQLiteRecordManager implements RecordManagerInterface { this.config = config } + 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 + } + private async getDataSource(): Promise<DataSource> { const { sqliteOptions } = this.config if (!sqliteOptions) { @@ -170,20 +182,21 @@ class SQLiteRecordManager implements RecordManagerInterface { try { const dataSource = await this.getDataSource() const queryRunner = dataSource.createQueryRunner() + const tableName = this.sanitizeTableName(this.tableName) await queryRunner.manager.query(` -CREATE TABLE IF NOT EXISTS "${this.tableName}" ( +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 "${this.tableName}" (updated_at); -CREATE INDEX IF NOT EXISTS key_index ON "${this.tableName}" (key); -CREATE INDEX IF NOT EXISTS namespace_index ON "${this.tableName}" (namespace); -CREATE INDEX IF NOT EXISTS group_id_index ON "${this.tableName}" (group_id);`) +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);`) await queryRunner.release() } catch (e: any) { @@ -219,6 +232,7 @@ CREATE INDEX IF NOT EXISTS group_id_index ON "${this.tableName}" (group_id);`) } 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 ?? {} @@ -241,7 +255,7 @@ CREATE INDEX IF NOT EXISTS group_id_index ON "${this.tableName}" (group_id);`) ]) const query = ` - INSERT INTO "${this.tableName}" (key, namespace, updated_at, group_id) + INSERT INTO "${tableName}" (key, namespace, updated_at, group_id) VALUES (?, ?, ?, ?) ON CONFLICT (key, namespace) DO UPDATE SET updated_at = excluded.updated_at` @@ -264,12 +278,13 @@ CREATE INDEX IF NOT EXISTS group_id_index ON "${this.tableName}" (group_id);`) if (keys.length === 0) { return [] } + const tableName = this.sanitizeTableName(this.tableName) // Prepare the placeholders and the query const placeholders = keys.map(() => `?`).join(', ') const sql = ` SELECT key - FROM "${this.tableName}" + FROM "${tableName}" WHERE namespace = ? AND key IN (${placeholders})` // Initialize an array to fill with the existence checks @@ -299,7 +314,9 @@ CREATE INDEX IF NOT EXISTS group_id_index ON "${this.tableName}" (group_id);`) async listKeys(options?: ListKeyOptions): Promise<string[]> { const { before, after, limit, groupIds } = options ?? {} - let query = `SELECT key FROM "${this.tableName}" WHERE namespace = ?` + const tableName = this.sanitizeTableName(this.tableName) + + let query = `SELECT key FROM "${tableName}" WHERE namespace = ?` const values: (string | number | string[])[] = [this.namespace] if (before) { @@ -350,9 +367,10 @@ CREATE INDEX IF NOT EXISTS group_id_index ON "${this.tableName}" (group_id);`) const dataSource = await this.getDataSource() const queryRunner = dataSource.createQueryRunner() + const tableName = this.sanitizeTableName(this.tableName) const placeholders = keys.map(() => '?').join(', ') - const query = `DELETE FROM "${this.tableName}" WHERE namespace = ? AND key IN (${placeholders});` + const query = `DELETE FROM "${tableName}" WHERE namespace = ? AND key IN (${placeholders});` const values = [this.namespace, ...keys].map((v) => (typeof v !== 'string' ? `${v}` : v)) // Directly using try/catch with async/await for cleaner flow
packages/components/nodes/sequentialagents/commonUtils.ts+11 −2 modified@@ -153,7 +153,13 @@ export const processImageMessage = async (llm: BaseChatModel, nodeData: INodeDat export const getVM = async (appDataSource: DataSource, databaseEntities: IDatabaseEntity, nodeData: INodeData, flow: ICommonObject) => { const variables = await getVars(appDataSource, databaseEntities, nodeData) - let sandbox: any = {} + let sandbox: any = { + util: undefined, + Symbol: undefined, + child_process: undefined, + fs: undefined, + process: undefined + } sandbox['$vars'] = prepareSandboxVars(variables) sandbox['$flow'] = flow @@ -169,7 +175,10 @@ export const getVM = async (appDataSource: DataSource, databaseEntities: IDataba require: { external: { modules: deps }, builtin: builtinDeps - } + }, + eval: false, + wasm: false, + timeout: 10000 } as any return new NodeVM(nodeVMOptions)
packages/components/nodes/sequentialagents/State/State.ts+11 −2 modified@@ -196,7 +196,13 @@ class State_SeqAgents implements INode { input } - let sandbox: any = {} + let sandbox: any = { + util: undefined, + Symbol: undefined, + child_process: undefined, + fs: undefined, + process: undefined + } sandbox['$vars'] = prepareSandboxVars(variables) sandbox['$flow'] = flow @@ -212,7 +218,10 @@ class State_SeqAgents implements INode { require: { external: { modules: deps }, builtin: builtinDeps - } + }, + eval: false, + wasm: false, + timeout: 10000 } as any const vm = new NodeVM(nodeVMOptions)
packages/components/nodes/tools/ChatflowTool/ChatflowTool.ts+13 −2 modified@@ -318,7 +318,15 @@ class ChatflowTool extends StructuredTool { body: JSON.stringify(body) } - let sandbox = { $callOptions: options, $callBody: body } + let sandbox = { + $callOptions: options, + $callBody: body, + util: undefined, + Symbol: undefined, + child_process: undefined, + fs: undefined, + process: undefined + } const code = ` const fetch = require('node-fetch'); @@ -349,7 +357,10 @@ try { require: { external: { modules: deps }, builtin: builtinDeps - } + }, + eval: false, + wasm: false, + timeout: 10000 } as any const vm = new NodeVM(vmOptions)
packages/components/nodes/tools/CustomTool/core.ts+11 −2 modified@@ -111,7 +111,13 @@ export class DynamicStructuredTool< _?: CallbackManagerForToolRun, flowConfig?: { sessionId?: string; chatId?: string; input?: string; state?: ICommonObject } ): Promise<string> { - let sandbox: any = {} + let sandbox: any = { + util: undefined, + Symbol: undefined, + child_process: undefined, + fs: undefined, + process: undefined + } if (typeof arg === 'object' && Object.keys(arg).length) { for (const item in arg) { sandbox[`$${item}`] = arg[item] @@ -137,7 +143,10 @@ export class DynamicStructuredTool< require: { external: { modules: deps }, builtin: builtinDeps - } + }, + eval: false, + wasm: false, + timeout: 10000 } as any const vm = new NodeVM(options)
packages/components/nodes/tools/OpenAPIToolkit/core.ts+11 −2 modified@@ -196,7 +196,13 @@ export class DynamicStructuredTool< _?: CallbackManagerForToolRun, flowConfig?: { sessionId?: string; chatId?: string; input?: string; state?: ICommonObject } ): Promise<string> { - let sandbox: any = {} + let sandbox: any = { + util: undefined, + Symbol: undefined, + child_process: undefined, + fs: undefined, + process: undefined + } if (typeof arg === 'object' && Object.keys(arg).length) { for (const item in arg) { sandbox[`$${item}`] = arg[item] @@ -237,7 +243,10 @@ export class DynamicStructuredTool< require: { external: { modules: deps }, builtin: builtinDeps - } + }, + eval: false, + wasm: false, + timeout: 10000 } as any const vm = new NodeVM(options)
packages/components/nodes/utilities/CustomFunction/CustomFunction.ts+12 −2 modified@@ -117,7 +117,14 @@ class CustomFunction_Utilities implements INode { } } - let sandbox: any = { $input: input } + let sandbox: any = { + $input: input, + util: undefined, + Symbol: undefined, + child_process: undefined, + fs: undefined, + process: undefined + } sandbox['$vars'] = prepareSandboxVars(variables) sandbox['$flow'] = flow sandbox['$tools'] = tools @@ -140,7 +147,10 @@ class CustomFunction_Utilities implements INode { require: { external: { modules: deps }, builtin: builtinDeps - } + }, + eval: false, + wasm: false, + timeout: 10000 } as any const vm = new NodeVM(nodeVMOptions)
packages/components/nodes/utilities/IfElseFunction/IfElseFunction.ts+12 −2 modified@@ -119,7 +119,14 @@ class IfElseFunction_Utilities implements INode { } } - let sandbox: any = { $input: input } + let sandbox: any = { + $input: input, + util: undefined, + Symbol: undefined, + child_process: undefined, + fs: undefined, + process: undefined + } sandbox['$vars'] = prepareSandboxVars(variables) sandbox['$flow'] = flow @@ -141,7 +148,10 @@ class IfElseFunction_Utilities implements INode { require: { external: { modules: deps }, builtin: builtinDeps - } + }, + eval: false, + wasm: false, + timeout: 10000 } as any const vm = new NodeVM(nodeVMOptions)
packages/components/nodes/vectorstores/Postgres/driver/Base.ts+13 −1 modified@@ -28,13 +28,25 @@ export abstract class VectorStoreDriver { } getTableName() { - return getTableName(this.nodeData) + return this.sanitizeTableName(getTableName(this.nodeData)) } getEmbeddings() { return this.nodeData.inputs?.embeddings as Embeddings } + 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 getCredentials() { const credentialData = await getCredentialData(this.nodeData.credential ?? '', this.options) const user = getCredentialParam('user', credentialData, this.nodeData, process.env.POSTGRES_VECTORSTORE_USER)
packages/components/src/storageUtils.ts+4 −2 modified@@ -122,10 +122,12 @@ export const addSingleFileToStorage = async (mime: string, bf: Buffer, fileName: export const getFileFromStorage = async (file: string, ...paths: string[]): Promise<Buffer> => { const storageType = getStorageType() + const sanitizedFilename = _sanitizeFilename(file) + if (storageType === 's3') { const { s3Client, Bucket } = getS3Config() - let Key = paths.reduce((acc, cur) => acc + '/' + cur, '') + '/' + file + let Key = paths.reduce((acc, cur) => acc + '/' + cur, '') + '/' + sanitizedFilename if (Key.startsWith('/')) { Key = Key.substring(1) } @@ -147,7 +149,7 @@ export const getFileFromStorage = async (file: string, ...paths: string[]): Prom const buffer = Buffer.concat(response.Body.toArray()) return buffer } else { - const fileInStorage = path.join(getStoragePath(), ...paths, file) + const fileInStorage = path.join(getStoragePath(), ...paths, sanitizedFilename) return fs.readFileSync(fileInStorage) } }
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-gjx9-wg9x-7gvpghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-29189ghsaADVISORY
- drive.google.com/file/d/1WHPslTmQmAM9xPJifULS2qAo7hcidB4L/viewghsaWEB
- github.com/FlowiseAI/Flowise/commit/9a417bdc95f58d6dd92cbf60dad42414aba34754ghsaWEB
- github.com/FlowiseAI/Flowise/pull/3818ghsaWEB
News mentions
0No linked articles in our index yet.