Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { SqlDatabase } from '@langchain/classic/sql_db'
import { ICommonObject, INode, INodeData, INodeParams, IServerSideEventStreamer } from '../../../src/Interface'
import { ConsoleCallbackHandler, CustomChainHandler, additionalCallbacks } from '../../../src/handler'
import { getBaseClasses, getInputVariables, transformBracesWithColon } from '../../../src/utils'
import { validateSQLitePath } from '../../../src/validator'
import { checkInputs, Moderation, streamResponse } from '../../moderation/Moderation'
import { formatResponse } from '../../outputparsers/OutputParserHelpers'

Expand Down Expand Up @@ -223,7 +224,7 @@ const getSQLDBChain = async (
databaseType === 'sqlite'
? {
type: databaseType,
database: url
database: validateSQLitePath(url)
}
: ({
type: databaseType,
Expand Down
4 changes: 4 additions & 0 deletions packages/components/nodes/memory/AgentMemory/AgentMemory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SqliteSaver } from './SQLiteAgentMemory/sqliteSaver'
import { DataSource } from 'typeorm'
import { PostgresSaver } from './PostgresAgentMemory/pgSaver'
import { MySQLSaver } from './MySQLAgentMemory/mysqlSaver'
import { sanitizeDataSourceOptions } from '../../../src/sanitizeDataSourceOptions'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Import validateSQLitePath to secure the user-provided databaseFilePath against path traversal and arbitrary file write vulnerabilities.

Suggested change
import { sanitizeDataSourceOptions } from '../../../src/sanitizeDataSourceOptions'
import { sanitizeDataSourceOptions } from '../../../src/sanitizeDataSourceOptions'
import { validateSQLitePath } from '../../../src/validator'


class AgentMemory_Memory implements INode {
label: string
Expand Down Expand Up @@ -96,6 +97,8 @@ class AgentMemory_Memory implements INode {
label: 'Additional Connection Configuration',
name: 'additionalConfig',
type: 'json',
description:
'Optional TypeORM connection options (e.g. ssl, connectTimeout). entities, subscribers, migrations, and extra are not allowed.',
additionalParams: true,
optional: true
}
Expand All @@ -118,6 +121,7 @@ class AgentMemory_Memory implements INode {
} catch (exception) {
throw new Error('Invalid JSON in the Additional Configuration: ' + exception)
}
additionalConfiguration = sanitizeDataSourceOptions(additionalConfiguration)
}
Comment on lines +124 to 125
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The databaseFilePath input is user-controlled and is resolved directly using path.resolve(databaseFilePath) without validation if SQLite is selected. This allows path traversal and arbitrary file write/read vulnerabilities.

Validate databaseFilePath using validateSQLitePath if it is provided.

            additionalConfiguration = sanitizeDataSourceOptions(additionalConfiguration)
        }

        if (databaseType === 'sqlite' && databaseFilePath) {
            validateSQLitePath(databaseFilePath)
        }


const threadId = options.sessionId || options.chatId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SaverOptions } from '../interface'
import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeParams } from '../../../../src/Interface'
import { DataSource } from 'typeorm'
import { MySQLSaver } from './mysqlSaver'
import { sanitizeDataSourceOptions } from '../../../../src/sanitizeDataSourceOptions'

class MySQLAgentMemory_Memory implements INode {
label: string
Expand Down Expand Up @@ -54,6 +55,8 @@ class MySQLAgentMemory_Memory implements INode {
label: 'Additional Connection Configuration',
name: 'additionalConfig',
type: 'json',
description:
'Optional TypeORM connection options (e.g. ssl, connectTimeout). entities, subscribers, migrations, and extra are not allowed.',
additionalParams: true,
optional: true
}
Expand All @@ -74,6 +77,7 @@ class MySQLAgentMemory_Memory implements INode {
} catch (exception) {
throw new Error('Invalid JSON in the Additional Configuration: ' + exception)
}
additionalConfiguration = sanitizeDataSourceOptions(additionalConfiguration)
}

const threadId = options.sessionId || options.chatId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SaverOptions } from '../interface'
import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeParams } from '../../../../src/Interface'
import { DataSource } from 'typeorm'
import { PostgresSaver } from './pgSaver'
import { sanitizeDataSourceOptions } from '../../../../src/sanitizeDataSourceOptions'

class PostgresAgentMemory_Memory implements INode {
label: string
Expand Down Expand Up @@ -54,6 +55,8 @@ class PostgresAgentMemory_Memory implements INode {
label: 'Additional Connection Configuration',
name: 'additionalConfig',
type: 'json',
description:
'Optional TypeORM connection options (e.g. ssl, connectTimeout). entities, subscribers, migrations, and extra are not allowed.',
additionalParams: true,
optional: true
}
Expand All @@ -74,6 +77,7 @@ class PostgresAgentMemory_Memory implements INode {
} catch (exception) {
throw new Error('Invalid JSON in the Additional Configuration: ' + exception)
}
additionalConfiguration = sanitizeDataSourceOptions(additionalConfiguration)
}

const threadId = options.sessionId || options.chatId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { SaverOptions } from '../interface'
import { ICommonObject, IDatabaseEntity, INode, INodeData, INodeParams } from '../../../../src/Interface'
import { SqliteSaver } from './sqliteSaver'
import { DataSource } from 'typeorm'
import { sanitizeDataSourceOptions } from '../../../../src/sanitizeDataSourceOptions'

class SQLiteAgentMemory_Memory implements INode {
label: string
Expand Down Expand Up @@ -40,6 +41,8 @@ class SQLiteAgentMemory_Memory implements INode {
label: 'Additional Connection Configuration',
name: 'additionalConfig',
type: 'json',
description:
'Optional TypeORM connection options (e.g. ssl, connectTimeout). entities, subscribers, migrations, and extra are not allowed.',
additionalParams: true,
optional: true
}
Expand All @@ -60,6 +63,7 @@ class SQLiteAgentMemory_Memory implements INode {
} catch (exception) {
throw new Error('Invalid JSON in the Additional Configuration: ' + exception)
}
additionalConfiguration = sanitizeDataSourceOptions(additionalConfiguration)
}

const threadId = options.sessionId || options.chatId
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ICommonObject, INode, INodeData, INodeParams } from '../../../src/Interface'
import { getBaseClasses, getCredentialData, getCredentialParam } from '../../../src/utils'
import { sanitizeDataSourceOptions } from '../../../src/sanitizeDataSourceOptions'
import { ListKeyOptions, RecordManagerInterface, UpdateOptions } from '@langchain/community/indexes/base'
import { DataSource } from 'typeorm'

Expand Down Expand Up @@ -47,6 +48,8 @@ class MySQLRecordManager_RecordManager implements INode {
label: 'Additional Connection Configuration',
name: 'additionalConfig',
type: 'json',
description:
'Optional TypeORM connection options (e.g. ssl, connectTimeout). entities, subscribers, migrations, and extra are not allowed.',
additionalParams: true,
optional: true
},
Expand Down Expand Up @@ -133,6 +136,7 @@ class MySQLRecordManager_RecordManager implements INode {
} catch (exception) {
throw new Error('Invalid JSON in the Additional Configuration: ' + exception)
}
additionalConfiguration = sanitizeDataSourceOptions(additionalConfiguration)
}

const mysqlOptions = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ListKeyOptions, RecordManagerInterface, UpdateOptions } from '@langchai
import { DataSource } from 'typeorm'
import { getHost, getSSL } from '../../vectorstores/Postgres/utils'
import { getDatabase, getPort, getTableName } from './utils'
import { sanitizeDataSourceOptions } from '../../../src/sanitizeDataSourceOptions'

const serverCredentialsExists = !!process.env.POSTGRES_RECORDMANAGER_USER && !!process.env.POSTGRES_RECORDMANAGER_PASSWORD

Expand Down Expand Up @@ -63,6 +64,8 @@ class PostgresRecordManager_RecordManager implements INode {
label: 'Additional Connection Configuration',
name: 'additionalConfig',
type: 'json',
description:
'Optional TypeORM connection options (e.g. ssl, connectTimeout). entities, subscribers, migrations, and extra are not allowed.',
additionalParams: true,
optional: true
},
Expand Down Expand Up @@ -149,6 +152,7 @@ class PostgresRecordManager_RecordManager implements INode {
} catch (exception) {
throw new Error('Invalid JSON in the Additional Configuration: ' + exception)
}
additionalConfiguration = sanitizeDataSourceOptions(additionalConfiguration)
}

const postgresConnectionOptions = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getBaseClasses, getUserHome } from '../../../src/utils'
import { ListKeyOptions, RecordManagerInterface, UpdateOptions } from '@langchain/community/indexes/base'
import { DataSource } from 'typeorm'
import path from 'path'
import { sanitizeDataSourceOptions } from '../../../src/sanitizeDataSourceOptions'

class SQLiteRecordManager_RecordManager implements INode {
label: string
Expand Down Expand Up @@ -36,6 +37,8 @@ class SQLiteRecordManager_RecordManager implements INode {
label: 'Additional Connection Configuration',
name: 'additionalConfig',
type: 'json',
description:
'Optional TypeORM connection options (e.g. ssl, connectTimeout). entities, subscribers, migrations, and extra are not allowed.',
additionalParams: true,
optional: true
},
Expand Down Expand Up @@ -113,6 +116,7 @@ class SQLiteRecordManager_RecordManager implements INode {
} catch (exception) {
throw new Error('Invalid JSON in the Additional Configuration: ' + exception)
}
additionalConfiguration = sanitizeDataSourceOptions(additionalConfiguration)
}

const database = path.join(process.env.DATABASE_PATH ?? path.join(getUserHome(), '.flowise'), 'database.sqlite')
Expand Down
2 changes: 2 additions & 0 deletions packages/components/nodes/vectorstores/Postgres/Postgres.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ class Postgres_VectorStores implements INode {
label: 'Additional Configuration',
name: 'additionalConfig',
type: 'json',
description:
'Optional TypeORM connection options (e.g. ssl, connectTimeout). entities, subscribers, migrations, and extra are not allowed.',
additionalParams: true,
optional: true
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import { VectorStoreDriver } from './Base'
import { FLOWISE_CHATID } from '../../../../src'
import { sanitizeDataSourceOptions } from '../../../../src/sanitizeDataSourceOptions'
import { DistanceStrategy, PGVectorStore, PGVectorStoreArgs } from '@langchain/community/vectorstores/pgvector'
import { Document } from '@langchain/core/documents'
import { PoolConfig } from 'pg'
Expand All @@ -27,6 +28,7 @@ export class PGVectorDriver extends VectorStoreDriver {
} catch (exception) {
throw new Error('Invalid JSON in the Additional Configuration: ' + exception)
}
additionalConfiguration = sanitizeDataSourceOptions(additionalConfiguration)
}

this._postgresConnectionOptions = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { DataSourceOptions } from 'typeorm'
import { VectorStoreDriver } from './Base'
import { FLOWISE_CHATID, ICommonObject } from '../../../../src'
import { sanitizeDataSourceOptions } from '../../../../src/sanitizeDataSourceOptions'
import { TypeORMVectorStore, TypeORMVectorStoreArgs, TypeORMVectorStoreDocument } from '@langchain/community/vectorstores/typeorm'
import { VectorStore } from '@langchain/core/vectorstores'
import { Document } from '@langchain/core/documents'
Expand All @@ -27,6 +28,7 @@ export class TypeORMDriver extends VectorStoreDriver {
} catch (exception) {
throw new Error('Invalid JSON in the Additional Configuration: ' + exception)
}
additionalConfiguration = sanitizeDataSourceOptions(additionalConfiguration)
}

this._postgresConnectionOptions = {
Expand Down
1 change: 1 addition & 0 deletions packages/components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export * from './agentflowv2Generator'
export * from './httpSecurity'
export * from './headerValidation'
export * from './pythonCodeValidator'
export * from './sanitizeDataSourceOptions'
export { MCPToolkit } from '../nodes/tools/MCP/core'
43 changes: 43 additions & 0 deletions packages/components/src/sanitizeDataSourceOptions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { sanitizeDataSourceOptions } from './sanitizeDataSourceOptions'

describe('sanitizeDataSourceOptions', () => {
it('returns empty object for empty input', () => {
expect(sanitizeDataSourceOptions({})).toEqual({})
})

it('returns empty object for nullish-like invalid input', () => {
expect(sanitizeDataSourceOptions(null as any)).toEqual({})
expect(sanitizeDataSourceOptions(undefined as any)).toEqual({})
})

it('passes through safe connection options', () => {
const config = {
ssl: { rejectUnauthorized: false },
connectTimeout: 10000,
poolSize: 5
}
expect(sanitizeDataSourceOptions(config)).toEqual(config)
})

it('returns a shallow copy and does not mutate the input', () => {
const config = { poolSize: 5 }
const result = sanitizeDataSourceOptions(config)
expect(result).toEqual(config)
expect(result).not.toBe(config)
})

describe('blocked keys', () => {
it.each(['entities', 'subscribers', 'migrations', 'extra'] as const)('throws when %s is present', (key) => {
expect(() => sanitizeDataSourceOptions({ [key]: ['/tmp/evil.js'] })).toThrow(`Disallowed TypeORM DataSource option: ${key}`)
})
Comment on lines +30 to +32
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add a test case to verify that blocked keys present on the prototype chain are also correctly detected and rejected.

        it.each(['entities', 'subscribers', 'migrations', 'extra'] as const)('throws when %s is present', (key) => {
            expect(() => sanitizeDataSourceOptions({ [key]: ['/tmp/evil.js'] })).toThrow("Disallowed TypeORM DataSource option: " + key)
        })

        it('throws when blocked key is present on the prototype chain', () => {
            const maliciousConfig = Object.create({ entities: ['/tmp/evil.js'] })
            expect(() => sanitizeDataSourceOptions(maliciousConfig)).toThrow('Disallowed TypeORM DataSource option: entities')
        })


it('throws when multiple blocked keys are present', () => {
expect(() =>
sanitizeDataSourceOptions({
entities: ['/tmp/a.js'],
extra: { foo: 'bar' }
})
).toThrow('Disallowed TypeORM DataSource option:')
})
})
})
24 changes: 24 additions & 0 deletions packages/components/src/sanitizeDataSourceOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ICommonObject } from './Interface'

/** TypeORM DataSource options that can load and execute arbitrary local JavaScript files. */
const BLOCKED_DATASOURCE_KEYS = ['entities', 'subscribers', 'migrations', 'extra'] as const

export type BlockedDataSourceKey = (typeof BLOCKED_DATASOURCE_KEYS)[number]

/**
* Rejects user-supplied TypeORM DataSource options that can lead to arbitrary code execution
* when passed to `new DataSource(options).initialize()`.
*/
export function sanitizeDataSourceOptions(config: ICommonObject): ICommonObject {
if (!config || typeof config !== 'object' || Array.isArray(config)) {
return {}
}

for (const key of BLOCKED_DATASOURCE_KEYS) {
if (Object.prototype.hasOwnProperty.call(config, key)) {
throw new Error(`Disallowed TypeORM DataSource option: ${key}`)
}
}
Comment on lines +21 to +25
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using Object.prototype.hasOwnProperty.call(config, key) only checks if the property is directly defined on the object itself. If a malicious payload is constructed such that the blocked keys (like entities or subscribers) are defined on the prototype chain (e.g., via prototype pollution or inheritance), this check will be bypassed, but TypeORM will still access and load them.

Using the in operator is much more secure as it inspects the prototype chain as well.

Suggested change
for (const key of BLOCKED_DATASOURCE_KEYS) {
if (Object.prototype.hasOwnProperty.call(config, key)) {
throw new Error(`Disallowed TypeORM DataSource option: ${key}`)
}
}
for (const key of BLOCKED_DATASOURCE_KEYS) {
if (key in config) {
throw new Error("Disallowed TypeORM DataSource option: " + key)
}
}


return { ...config }
}
Loading
Loading