Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
6 changes: 6 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,10 @@ ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
COPY --from=build /usr/local/lib/node_modules /usr/local/lib/node_modules
COPY --from=build /usr/local/bin /usr/local/bin

# Give the node user ownership of the installed package (node:*-alpine already has this user)
RUN chown -R node:node /usr/local/lib/node_modules /usr/local/bin

# Switch to non-root user
USER node

ENTRYPOINT ["flowise", "start"]
10 changes: 6 additions & 4 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ Starts Flowise from [DockerHub Image](https://hub.docker.com/r/flowiseai/flowise

If you like to persist your data (flows, logs, credentials, storage), set these variables in the `.env` file inside `docker` folder:

- DATABASE_PATH=/root/.flowise
- LOG_PATH=/root/.flowise/logs
- SECRETKEY_PATH=/root/.flowise
- BLOB_STORAGE_PATH=/root/.flowise/storage
- DATABASE_PATH=/home/node/.flowise
- LOG_PATH=/home/node/.flowise/logs
- SECRETKEY_PATH=/home/node/.flowise
- BLOB_STORAGE_PATH=/home/node/.flowise/storage

Flowise also support different environment variables to configure your instance. Read [more](https://docs.flowiseai.com/configuration/environment-variables)

> The container runs as the non-root `node` user (uid 1000), whose home directory is `/home/node`. If you bind-mount a host directory (e.g. `~/.flowise`) for persistence, make sure it's writable by that user - on Linux hosts this may require `chown -R 1000:1000 ~/.flowise`.

## Queue Mode:

### Building from source:
Expand Down
11 changes: 7 additions & 4 deletions docker/docker-compose-queue-prebuilt.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@ services:
ports:
- '${PORT:-3000}:${PORT:-3000}'
volumes:
- ~/.flowise:/root/.flowise
# Container runs as the non-root `node` user, whose home is /home/node.
# Ensure the host directory is writable by that user (e.g. `chown -R 1000:1000 ~/.flowise` on Linux hosts).
- ~/.flowise:/home/node/.flowise
environment:
# --- Essential Flowise Vars ---
- PORT=${PORT:-3000}
- DATABASE_PATH=${DATABASE_PATH:-/root/.flowise}
- DATABASE_PATH=${DATABASE_PATH:-/home/node/.flowise}
- DATABASE_TYPE=${DATABASE_TYPE}
- DATABASE_PORT=${DATABASE_PORT}
- DATABASE_HOST=${DATABASE_HOST}
Expand Down Expand Up @@ -183,11 +185,12 @@ services:
container_name: flowise-worker
restart: always
volumes:
- ~/.flowise:/root/.flowise
# Container runs as the non-root `node` user, whose home is /home/node.
- ~/.flowise:/home/node/.flowise
environment:
# --- Essential Flowise Vars ---
- WORKER_PORT=${WORKER_PORT:-5566}
- DATABASE_PATH=${DATABASE_PATH:-/root/.flowise}
- DATABASE_PATH=${DATABASE_PATH:-/home/node/.flowise}
- DATABASE_TYPE=${DATABASE_TYPE}
- DATABASE_PORT=${DATABASE_PORT}
- DATABASE_HOST=${DATABASE_HOST}
Expand Down
27 changes: 15 additions & 12 deletions docker/docker-compose-queue-source.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@ services:
ports:
- '${PORT}:${PORT}'
volumes:
# Mount local .flowise to container's default location
- ../.flowise:/root/.flowise
# Mount local .flowise to container's default location.
# Container runs as the non-root `node` user, whose home is /home/node.
# Ensure the host directory is writable by that user (e.g. `chown -R 1000:1000 ../.flowise` on Linux hosts).
- ../.flowise:/home/node/.flowise
environment:
# --- Essential Flowise Vars ---
- PORT=${PORT:-3000}
- DATABASE_PATH=/root/.flowise
- SECRETKEY_PATH=/root/.flowise
- LOG_PATH=/root/.flowise/logs
- BLOB_STORAGE_PATH=/root/.flowise/storage
- DATABASE_PATH=/home/node/.flowise
- SECRETKEY_PATH=/home/node/.flowise
- LOG_PATH=/home/node/.flowise/logs
- BLOB_STORAGE_PATH=/home/node/.flowise/storage
# --- Queue Vars (Main Instance) ---
- MODE=queue
- QUEUE_NAME=flowise-queue # Ensure this matches worker
Expand All @@ -43,15 +45,16 @@ services:
context: .. # Build context is still the root
dockerfile: docker/worker/Dockerfile # Ensure this path is correct
volumes:
# Mount same local .flowise to worker
- ../.flowise:/root/.flowise
# Mount same local .flowise to worker.
# Container runs as the non-root `node` user, whose home is /home/node.
- ../.flowise:/home/node/.flowise
environment:
# --- Essential Flowise Vars ---
- WORKER_PORT=${WORKER_PORT:-5566} # Port for worker healthcheck
- DATABASE_PATH=/root/.flowise
- SECRETKEY_PATH=/root/.flowise
- LOG_PATH=/root/.flowise/logs
- BLOB_STORAGE_PATH=/root/.flowise/storage
- DATABASE_PATH=/home/node/.flowise
- SECRETKEY_PATH=/home/node/.flowise
- LOG_PATH=/home/node/.flowise/logs
- BLOB_STORAGE_PATH=/home/node/.flowise/storage
# --- Queue Vars (Main Instance) ---
- MODE=queue
- QUEUE_NAME=flowise-queue # Ensure this matches worker
Expand Down
4 changes: 3 additions & 1 deletion docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -160,5 +160,7 @@ services:
retries: 5
start_period: 30s
volumes:
- ~/.flowise:/root/.flowise
# Container now runs as the non-root `node` user, whose home is /home/node.
# Ensure the host directory is writable by that user (e.g. `chown -R 1000:1000 ~/.flowise` on Linux hosts).
- ~/.flowise:/home/node/.flowise
entrypoint: /bin/sh -c "sleep 3; flowise start"
6 changes: 6 additions & 0 deletions docker/worker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,11 @@ ENV WORKER_PORT=5566
# Expose port (can be overridden by env var)
EXPOSE ${WORKER_PORT}

# Give the node user ownership of the application files (node:*-alpine already has this user)
RUN chown -R node:node /usr/src /app

# Switch to non-root user
USER node

# Start healthcheck in background and flowise worker in foreground
CMD ["/bin/sh", "-c", "node /app/healthcheck/healthcheck.js & sleep 5 && pnpm run start-worker"]
4 changes: 3 additions & 1 deletion docker/worker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -159,5 +159,7 @@ services:
retries: 5
start_period: 30s
volumes:
- ~/.flowise:/root/.flowise
# Container now runs as the non-root `node` user, whose home is /home/node.
# Ensure the host directory is writable by that user (e.g. `chown -R 1000:1000 ~/.flowise` on Linux hosts).
- ~/.flowise:/home/node/.flowise
entrypoint: /bin/sh -c "node /app/healthcheck/healthcheck.js & sleep 5 && pnpm run start-worker"
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +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 { assertReadOnlySqlStatement, validateSQLitePath } from '../../../src/validator'
import { checkInputs, Moderation, streamResponse } from '../../moderation/Moderation'
import { formatResponse } from '../../outputparsers/OutputParserHelpers'

Expand Down Expand Up @@ -67,7 +67,9 @@ class SqlDatabaseChain_Chains implements INode {
label: 'Connection string or file path (sqlite only)',
name: 'url',
type: 'string',
placeholder: '127.0.0.1:5432/chinook'
placeholder: '127.0.0.1:5432/chinook',
warning:
'This chain executes LLM-generated SQL directly against the database. For SQLite, only point this at a trusted, disposable database file — queries are restricted to read-only SELECT/WITH statements, but the underlying file should not be shared with other sensitive data.'
},
{
label: 'Include Tables',
Expand Down Expand Up @@ -232,6 +234,17 @@ const getSQLDBChain = async (
} as DataSourceOptions)
)

if (databaseType === 'sqlite') {
// Restrict every query run against this connection - including langchain's own
// schema introspection and the LLM-generated query - to a single read-only
// SELECT/WITH statement. See assertReadOnlySqlStatement for why.
const originalQuery = datasource.query.bind(datasource)
datasource.query = (async (sql: string, parameters?: any[], queryRunner?: any) => {
assertReadOnlySqlStatement(sql)
return originalQuery(sql, parameters, queryRunner)
}) as typeof datasource.query
}

const db = await SqlDatabase.fromDataSourceParams({
appDataSource: datasource,
includesTables: includesTables,
Expand Down
47 changes: 47 additions & 0 deletions packages/components/src/validator.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
assertReadOnlySqlStatement,
getSafeFilePath,
isPathTraversal,
isUnsafeFilePath,
Expand Down Expand Up @@ -796,3 +797,49 @@ describe('getSafeFilePath', () => {
})
})
})

describe('assertReadOnlySqlStatement', () => {
describe('accepts read-only statements', () => {
it.each([
['simple select', 'SELECT * FROM users'],
['select with trailing semicolon', 'SELECT id FROM users;'],
['lowercase select', 'select id from users'],
['select with where clause', "SELECT id FROM users WHERE name = 'bob'"],
['cte with with clause', 'WITH t AS (SELECT 1) SELECT * FROM t'],
[
'sqlite introspection query using pragma_table_info as a table-valued function',
"SELECT m.name AS table_name, p.name AS column_name FROM sqlite_master m JOIN pragma_table_info(m.name) p WHERE m.type = 'table'"
]
])('should not throw for %s', (_desc, sql) => {
expect(() => assertReadOnlySqlStatement(sql)).not.toThrow()
})
})

describe('rejects write / dangerous statements', () => {
it.each([
['attach database', "ATTACH DATABASE '/etc/chromium/exploit.conf' AS x"],
['vacuum into', "VACUUM INTO '/etc/chromium/exploit.conf'"],
['bare pragma statement', 'PRAGMA journal_mode=WAL'],
['stacked statements', 'SELECT 1; DROP TABLE users'],
['stacked statements with attach', "SELECT 1; ATTACH DATABASE '/etc/chromium/exploit.conf' AS x"],
['insert', "INSERT INTO users (name) VALUES ('evil')"],
['update', "UPDATE users SET name = 'evil'"],
['delete', 'DELETE FROM users'],
['drop table', 'DROP TABLE users'],
['load_extension call', "SELECT load_extension('/tmp/evil.so')"],
['load_extension call mixed case', "SELECT LOAD_EXTENSION('/tmp/evil.so')"]
])('should throw for %s', (_desc, sql) => {
expect(() => assertReadOnlySqlStatement(sql)).toThrow()
})
})

describe('invalid input', () => {
it.each([
['empty string', ''],
['undefined', undefined],
['null', null]
])('should throw for %s', (_desc, sql) => {
expect(() => assertReadOnlySqlStatement(sql as any)).toThrow()
})
})
})
40 changes: 40 additions & 0 deletions packages/components/src/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,46 @@ export const validateSQLitePath = (userProvidedPath: string | undefined): string
return resolvedPath
}

/**
* Restricts SQL executed against a SQLite database opened via validateSQLitePath to a
* single read-only SELECT/WITH statement.
*
* The Sql Database Chain hands LLM-generated SQL directly to TypeORM's raw query
* executor with no statement-type filtering. Without this guard, a compromised or
* malicious LLM response can run `ATTACH DATABASE`/`VACUUM INTO`/bare `PRAGMA` to write
* arbitrary files anywhere the process can write, bypassing validateSQLitePath (which
* only constrains the initial connection path, not queries run afterward).
*
* All legitimate queries issued against sqlite by this chain (including langchain's own
* schema introspection, which uses `pragma_table_info()` as a table-valued function
* inside a SELECT) are single SELECT/WITH statements, so this restriction does not
* affect normal operation.
*
* @param {string} sql The SQL statement to validate
* @throws {Error} If the statement is not a single read-only SELECT/WITH statement
*/
export const assertReadOnlySqlStatement = (sql: string): void => {
if (!sql || typeof sql !== 'string') {
throw new Error('Invalid SQL statement: statement is required and must be a string')
}

let trimmed = sql.trim()
// Strip at most one trailing semicolon (+ trailing whitespace)
trimmed = trimmed.replace(/;\s*$/, '')

if (trimmed.includes(';')) {
throw new Error('Invalid SQL statement: multiple statements are not allowed')
}

if (!/^(SELECT|WITH)\b/i.test(trimmed)) {
throw new Error('Invalid SQL statement: only read-only SELECT/WITH statements are allowed')
}

if (/load_extension\s*\(/i.test(trimmed)) {
throw new Error('Invalid SQL statement: load_extension is not allowed')
}
}

/**
* Sanitize a file name to prevent path traversal attacks.
* Strips common storage prefixes, extracts the basename, runs it through
Expand Down
Loading