CodeVetter · Repo Unpack · Risk map
langflow-ai/langflow
The dominant risk theme is executing caller-influenced code: custom components are arbitrary Python exec'd at build/validation time, stored flow JSON embeds component source, and multiple policy layers (custom_component_admin_only, tweak policy, owner-only tweaks, catalog/model-provider policy) exist to contain that. Auth surface risk is managed but default-lenient (AUTO_LOGIN, permissive CORS with warnings, SSRF protection off by default, short SECRET_KEY tolerated with legacy decryption). Supply-chain and infra footguns include remote bundle zips at startup, entry-point service replacement, per-worker in-memory job queues, and pickle in the cache service.
- Custom components execute arbitrary Python: lfx/custom/validate.py calls exec() on component code in several places (function extraction, class validation), explicitly noqa'd S102; code execution tools (python_repl) run user code gated by ensure_code_execution_enabled() + LANGFLOW_ALLOW_CUSTOM_COMPONENTS / LANGFLOW_BLOCK_CODE_INTERPRETER_COMPONENTS.
- Stored flows carry component source: a regular user can persist component code via the flow-write API then execute it — mitigated by prepare_flow_build_for_user applying custom_component_admin_only policy to stored bytes on a detached copy before Graph.from_payload in both /run and /build paths.
- An opt-in QEMU microVM sandbox backend (LANGFLOW_SANDBOX_BACKEND=exec-sandbox, extra `langflow-base[sandbox]`) exists for code-execution components; plugin backends are gated by LANGFLOW_SANDBOX_BACKEND_PLUGINS; shutdown_sandbox is called in lifespan cleanup so recycled workers don't leak VMs.
- SSRF protection is OFF by default (LANGFLOW_SSRF_PROTECTION_ENABLED unset/false 'for backward compatibility'); when enabled it blocks private IP/localhost/metadata endpoints with an allowlist LANGFLOW_SSRF_ALLOWED_HOSTS — a meaningful default-off posture for a tool that fetches URLs.
- CORS defaults are permissive: wildcard origin + credentials historically enabled credentialed cross-site requests; code now force-disables credentials when '*' is present and warns that stricter defaults arrive in v2.0 — operators on current versions still get all-origins by default.
- AUTO_LOGIN creates a superuser with a generated random password (legacy default password is ignored/rotated) guarded by a FileLock; teardown deletes the unused default superuser when AUTO_LOGIN is off — the warning text notes auto-login mode should not be exposed since access implies code execution.
- SECRET_KEY handling: short secrets derive the Fernet key via SHA-256 for new encryption, but a MultiFernet retains a deterministic random.Random(seed)-derived legacy key for decryption — deployments with weak SECRET_KEYs keep weak key derivation, only warned about (MINIMUM_SECRET_KEY_LENGTH=32 warning).
- Remote bundle URLs (settings.bundle_urls) download zip archives (incl. GitHub repos) at startup and extract component files into temp dirs that are added to components_path — remote code is fetched and executed; errors are logged but never block startup.
- Entry-point plugin loading replaces service classes: any installed distribution can register `lfx.services` entry points; subclass validation exists only for AUTHORIZATION/CATALOG_POLICY/MODEL_PROVIDER_POLICY/POLICY_BUNDLE services, and lfx.toml config paths get override=True — a malicious/misconfigured package can swap less-guarded services.
- Multi-worker state hazards are extensive: in-memory job queues break POLLING/STREAMING event delivery (~half of GET /build/<job_id>/events land on the wrong worker), MCP-over-SSE is broken, rate limits multiply per worker, orphan sweep is node-local — the guard refuses boot but the bypass env var exists.
- Pickle is used intentionally in the cache service (ruff S301 waived for services/cache/*) — safe for local memory/SQLite but a deserialization risk if pointed at an untrusted/shared backend.
- worker.py is vestigial Celery code: test_celery/build_vertex tasks exist but process_graph_cached_task raises NotImplementedError('This task is not implemented yet') — dead execution path kept in the tree.
- main.py is a 1277-line hotspot concentrating env-gated startup ordering; several comments document past races (UnboundLocalError on temp_dirs masking early failures, issue #13634; preload gating via is_step_complete to skip work in forked workers) — edit with care, ordering is load-bearing. (inferred)
- MCP session manager spawns subprocesses for stdio servers; cleanup_mcp_sessions is deliberately first in the finally block so interrupted shutdowns still kill them; idle sessions are reaped by interval/timeout tunables clamped to prevent CPU spin.