mirror of
https://github.com/FlowiseAI/Flowise.git
synced 2026-10-12 02:02:17 +00:00
fix: flowise-598 (#6665)
* update: user and permissions in docker files * fix: sql database chain node
This commit is contained in:
@@ -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
@@ -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:
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user