fix: flowise-598 (#6665)

* update: user and permissions in docker files

* fix: sql database chain node
This commit is contained in:
Ilango
2026-07-27 17:40:21 +05:30
committed by GitHub
parent ababf4c8eb
commit 4ea391204a
10 changed files with 148 additions and 24 deletions
+6
View File
@@ -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"]
+6 -4
View File
@@ -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:
+7 -4
View File
@@ -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}
@@ -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}
+15 -12
View File
@@ -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
@@ -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
+3 -1
View File
@@ -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
View File
@@ -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"]
+3 -1
View File
@@ -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"
@@ -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'
@@ -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',
@@ -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,
+47
View File
@@ -1,4 +1,5 @@
import {
assertReadOnlySqlStatement,
getSafeFilePath,
isPathTraversal,
isUnsafeFilePath,
@@ -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
View File
@@ -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