Extends the multitenant lfx POC with two new modes:
- --attack: same-process code in the worker reuses the run token to
read OPENAI_API_KEY directly. Confirms the design's call-out that
process-level isolation alone does not protect trusted and untrusted
components from each other inside the same worker. Uses sha256/length
instead of leaking the secret value.
- --split-workers: per-vertex capability planning from the real lfx
graph. Walks vertices, derives variables:read:* scopes from each
vertex's load_from_db fields, mints per-vertex tokens, dispatches a
worker per vertex with MT_STOP_VERTEX_ID. Untrusted vertex probes are
denied at the Runtime API; the scoped Language Model vertex runs to
completion with only its own token.
Runtime API now logs variable_denied events alongside variable_read,
giving the audit story symmetry.
Fixes a return-shape bug in worker._text_from_results that wrapped the
chat output text in a redundant dict on the happy path.
Still stubbed: does not serialize arbitrary vertex outputs between
worker processes or replace lfx's core scheduler.
Working POC of the multitenant capability boundary applied to lfx's
existing service injection. Not for merge; lives under prototypes/.
- CapabilityVariableService implements VariableServiceProtocol and is
registered via @register_service(ServiceType.VARIABLE_SERVICE),
replacing lfx's default at runtime.
- The real Basic Prompting starter project runs through lfx in a
worker subprocess whose env has been scrubbed of DB and provider
credentials.
- A short-lived run token gates variable resolution. Granted scope:
the flow completes against real OpenAI. Empty scope: the shim
returns PermissionError and lfx surfaces a ComponentBuildError.
Also establishes prototypes/ as a lint-free sandbox in the root ruff
config and pre-commit hooks (ruff, detect-secrets), matching the
existing pattern for docs/ python examples.
chore: upgrade langchain-classic to 1.0.7
- Update version constraint from ~=1.0.0 to ~=1.0.7
- Fixes issues present in version 1.0.4
- Update uv.lock with new dependency resolution
Co-authored-by: Janardan S Kavia <janardanskavia@Janardans-MacBook-Pro.local>
* feat: upgrade Docker images to Python 3.14 (experimental)
- Update pyproject.toml: requires-python from <3.14 to <3.15
- Update all 5 Dockerfiles to use Python 3.14:
- Builder stage: python3.12-trixie-slim → python3.14-trixie-slim
- Runtime stage: python:3.12-slim-trixie → python:3.14-slim-trixie
- src/backend/base/pyproject.toml
- docker/build_and_push_base.Dockerfile
- docker/build_and_push.Dockerfile
- docker/build_and_push_backend.Dockerfile
- docker/build_and_push_with_extras.Dockerfile
- docker/build_and_push_ep.Dockerfile
Python 3.14.5 was released on May 10, 2026. Upgrading to the latest
Python version should help reduce CVE vulnerabilities in Docker images.
This is an experimental change to test Python 3.14 compatibility:
- Docker images will use Python 3.14
- CI/CD workflows still test on Python 3.10-3.13
- Nightly build will validate if dependencies work with 3.14
- Can be reverted quickly if issues are found
- Monitor nightly build for failures
- If successful, update CI/CD workflows to add Python 3.14 to test matrix
- If failures occur, revert and investigate compatibility issues
* chore: support Python 3.14
Bump requires-python upper bound to <3.15 across langflow, langflow-base,
lfx, and langflow-sdk, and add 3.14 to CI test matrices so PRs are gated
on 3.14 compatibility.
Conditional pins for transitive deps without 3.14 wheels at the existing
caps:
- onnxruntime: >=1.26 on 3.14 (existing <1.24 cap retained for 3.10)
- faiss-cpu: >=1.13.2 on 3.14 (existing ==1.9.0.post1 retained for <3.14)
* chore: marker-gate IBM watsonx packages for Python 3.14
ibm-watsonx-ai 1.3.x cannot import on Python 3.14: its StrEnum subclasses
override __init__ in a way that conflicts with 3.14's reworked enum
__set_name__ path (TypeError on KnowledgeBaseFieldRole class creation).
Cap ibm-watsonx-ai, langchain-ibm, and the ibm-watsonx-orchestrate-*
extras at python_version<'3.14' until upstream adapts. Watsonx component
imports are already lazy, so this surfaces only at component-access time
on 3.14, where the existing ImportError handler in
lfx.components.ibm.__init__ degrades it gracefully.
test_model_utils.py imports ChatWatsonx at module top to verify
get_model_name resolves model_id; guard that import so the test
module skips on 3.14 instead of breaking collection.
* [autofix.ci] apply automated fixes
* chore: upgrade remaining Docker images to Python 3.14
build_and_push*.Dockerfile already moved to 3.14 in the merge from
feat/upgrade-python-3.14-docker-images. Apply the same bump to the
dev, devcontainer, and lfx Docker images so all in-repo Dockerfiles
share one Python version.
* [autofix.ci] apply automated fixes
* chore: gate 3.14-broken extras to python_version<'3.14'
cuga (and its transitive fastembed -> py-rust-stemmers source build),
altk/agent-lifecycle-toolkit (re-pulls ibm-watsonx-ai which was already
gated), langchain-pinecone, langwatch, ragstack-ai-knowledge-store, and
OpenDsStar all pin themselves below 3.14 upstream. Without a marker
guard, the workspace lock still tries to install them and a Docker
`uv sync` then attempts source builds that require Rust (CI Docker
test failure with py-rust-stemmers 0.1.5).
Add python_version<'3.14' (or <'3.13' for ragstack) to each pin so
those packages are simply omitted from the 3.14 install set until
upstream catches up. uv pip check is now clean on 3.14.2.
* test: skip test_altk_agent on Python 3.14
altk (agent-lifecycle-toolkit) is gated to python_version<'3.14'
upstream and now via the langflow-base [altk] extra marker. The test
imports altk at module top to exercise ALTKAgentComponent; guard the
import so collection skips on 3.14 instead of failing.
* test: skip remaining altk tests on Python 3.14
Three more test modules import lfx.base.agents.altk_* at module top,
which transitively imports altk. Add the same module-level skip guard
as test_altk_agent.py:
- test_altk_agent_logic.py
- test_altk_agent_tool_conversion.py
- test_conversation_context_ordering.py
* fix(calculator): replace removed ast.Num with ast.Constant for Python 3.14
ast.Num was deprecated in Python 3.8 in favor of ast.Constant and
removed entirely in 3.14. The calculator tool and its core walker
crashed on 3.14 with 'module ast has no attribute Num'.
Switch the isinstance checks to ast.Constant + isinstance(node.value,
(int, float)), and drop the now-dead ast.Num backwards-compat branch
in calculator_core.py (the ast.Constant branch already handles every
input it would have matched). Update the parser unit-test fixtures
to construct ast.Constant nodes.
* [autofix.ci] apply automated fixes
* [autofix.ci] apply automated fixes (attempt 2/3)
* test: skip LangWatch HTTP instrumentation tests on Python 3.14
langwatch is gated to python_version<'3.14' upstream. The
TestLangWatchHttpInstrumentation class patches langwatch.setup and
langwatch.trace inside a fixture, which requires langwatch to be
importable; on 3.14 the test errors with ModuleNotFoundError.
Guard the class with a skipif on langwatch availability.
* test: allow IBM and altk components to be missing on Python 3.14
test_all_modules_importable enforces that every component in __all__
imports cleanly. On 3.14 the ibm-watsonx-ai, langchain-ibm, and altk
packages are gated upstream and intentionally not installed, so the
WatsonxAI, WatsonxEmbeddings, and ALTKAgent components fail to import
as designed.
- test_all_components_in_categories_importable: accept a known-gated
deny-list on 3.14 instead of failing.
- test_all_lfx_component_modules_directly_importable: extend the
existing 'missing optional dependency' allowlist with altk,
langchain_ibm, and ibm_watsonx_ai.
* fix(lfx): pass ensure_exists to user_cache_dir for Python 3.14
Python 3.14 tightened PurePath.__init__ to reject unknown keyword
arguments. The KeyedWorkerLockManager constructor was passing
ensure_exists=True to Path() instead of user_cache_dir(), which
silently worked on 3.10-3.13 but raises TypeError on 3.14 and also
meant the cache directory was never actually being created.
Move the kwarg to user_cache_dir() where it belongs and update the
two unit tests that asserted the buggy call shape.
* [autofix.ci] apply automated fixes
* test: accept either ZIP or JSON error path on Python 3.14
Python 3.14's zipfile.is_zipfile() now validates the central-directory
signature in addition to EOCD, so the garbage+EOCD payload no longer
passes the is_zipfile() dispatch in the /flows/upload/ route. The
endpoint still returns 400 with a descriptive detail (now from the JSON
branch); only the message wording differs, which the test was asserting
verbatim.
* fix(lfx): normalize cors_origins ['*'] back to '*' on Python 3.14
Pydantic-settings on Python 3.14 parses the env var '*' into ['*']
before the cors_origins field validator runs (the list[str] | str union
resolves differently than on 3.10-3.13). Collapse that back to the
bare-string wildcard so downstream consumers — including
warn_about_future_cors_changes and the test suite — see the same shape
on every supported Python version.
---------
Co-authored-by: vijay kumar katuri <vijay.katuri@ibm.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
(cherry picked from commit 1955f8fae5)
* fix: pin postgres image to bookworm in docker_example to prevent collation mismatch
The postgres:16 tag silently moved its base from Debian Bookworm
(glibc 2.36) to Trixie (glibc 2.41), causing a recurring collation
version mismatch warning on existing langflow-postgres volumes.
Pin to postgres:16-bookworm in both docker-compose files and update
the README so existing data volumes keep matching the OS locale data.
Refs: https://github.com/langflow-ai/langflow/issues/9608
* docs: pin postgres image to bookworm in current docs compose snippets
Mirror the docker_example pin in the four current docs that publish
copyable Compose snippets pairing postgres:16 with a persistent
langflow-postgres volume. Prevents the same Bookworm-to-Trixie
collation version mismatch warning when users follow docs instead
of docker_example.
Versioned historical docs are left as-is.
Refs: https://github.com/langflow-ai/langflow/issues/9608
* fix: switch postgres pin from bookworm to trixie for OS consistency
Aligns the pinned postgres base with the langflow runtime image, which
moved to Debian Trixie in #12990. Keeping postgres on bookworm would
have diverged the stack and locked the database to an aging glibc that
will receive fewer security backports as bookworm ages into oldstable.
The pin itself still solves the original bug — postgres:16 cannot
silently roll its OS underneath an existing volume.
Document the one-time REFRESH COLLATION VERSION step for users
upgrading from a bookworm-initialized volume in docker_example/README.md.
Refs: https://github.com/langflow-ai/langflow/issues/9608
(cherry picked from commit 7504eb4c72)
* fix: stabilize model toggle and refresh Agent dropdown after provider changes
Two related regressions in the Model Provider management flow on
release-1.9.3:
1. Toggle bounce — rapid clicks on a model Switch flickered between on
and off before settling. ``handleModelToggle`` performed an
optimistic ``setQueryData`` on ``useGetEnabledModels`` but never
cancelled in-flight refetches. With React Query defaults
(``staleTime: 0``, ``refetchOnWindowFocus: true``), a background
refetch could land mid-debounce and overwrite the optimistic state
with stale server data, then the deferred mutation eventually
corrected it. Fix: ``cancelQueries`` before each optimistic update,
per the canonical TanStack Query optimistic-updates pattern.
2. Agent dropdown stayed stale after the provider modal closed —
disabled models remained listed in the Agent's Language Model
dropdown. The post-close ``isRefreshingAfterClose`` loading gate
only waited for ``useGetModelProviders`` to settle, not
``useGetEnabledModels``. When the providers refetch finished first,
the loading state cleared and ``groupedOptions`` ran against the
still-stale enabled-models cache. Fix: track both queries'
``isFetching`` flags in the gate's effect.
No behavioral change to the sticky-default UX — the user's previously
selected model continues to show in the dropdown with the wrench
affordance when globally disabled, as designed.
Also narrows the pre-existing ``catch (error: any)`` patterns in the
edited hook file to ``unknown`` with a small shared ``getErrorMessage``
helper, satisfying the staged-file no-explicit-any pre-commit lint.
Tests:
- New ``useProviderConfiguration`` unit test asserts
``cancelQueries`` runs before ``setQueryData`` on toggle.
- Extended ``ModelInputComponent`` test covers the dual-query loading
gate (providers settles first, enabled-models still in flight →
loading persists; then enabled-models settles → loading clears).
* fix: re-apply pending overlay through entire mutation lifecycle
Addresses review feedback on #13113: ``cancelQueries`` only cancels
refetches that are already in flight at click time. A new
``useGetEnabledModels`` refetch can still start during the 1s
debounce window (or while the mutation is in flight) and overwrite
the optimistic cache with stale server state, producing the same
bounce we just fixed.
Two coordinated changes keep the optimistic state protected for the
entire pending-toggle window:
1. ``pendingModelToggles`` is no longer cleared upfront when the
debounced flush sends a mutation. Instead, ``clearSentToggles``
removes only the entries whose value still matches what we sent
on ``onSettled`` / ``onError``. This preserves the overlay across
the in-flight mutation window AND correctly handles the case
where the user re-toggled the same model mid-flight (now a fresh
intent that survives clearing).
2. A new ``useEffect`` subscribes to ``useGetEnabledModels`` and
re-applies the pending overlay whenever the data emission drifts
from the user's pending intent. This catches any refetch
(window focus, mount, reconnect, stale-time expiry) that lands
between click and ``onSettled``, regardless of when it started.
Tests:
- New ``re-applies the pending overlay when a refetch surfaces
stale data`` test simulates a stale refetch and asserts the
effect re-applies the optimistic overlay.
- New ``does not re-overlay when no toggles are pending`` test
guards against spurious overlay calls on mount.
* fix: split overlay vs unsent buffers to prevent duplicate model toggle mutations
Addresses a follow-up race introduced by the previous commit: keeping
``pendingModelToggles`` populated through ``onSettled`` (so the
re-overlay effect could repel mid-flight refetches) also caused the
next flush to snapshot the in-flight entries again, sending duplicate
requests with non-deterministic success/failure ordering. The same
risk applied to ``flushPendingChanges`` on modal close.
Split the single buffer into two refs with single responsibilities:
- ``overlayToggles``: the union of every toggle still protecting the
UI. The re-overlay effect re-applies this whenever
``useGetEnabledModels`` emits new data. Drained per-key on
``onSettled``/``onError`` only when the overlay value still matches
what we sent (a user re-toggle mid-flight becomes a fresh intent
and must survive the clear).
- ``unsentToggles``: the strict subset that has NOT been sent in a
mutation yet, or was re-toggled since the last send. Drained
immediately at flush time so subsequent flushes never resend an
in-flight payload.
``handleModelToggle`` populates both buffers; flushes drain only
``unsentToggles`` and use ``overlayToggles`` for cache protection.
Two new unit tests:
- ``does not resend in-flight toggles when a new toggle is flushed``
asserts that after toggling A then B (with A's mutation in flight),
the second flush carries ONLY B.
- ``re-sends a model when the user re-toggles it after the previous
flush fired`` asserts that re-toggling the same model produces a
second mutation (the re-toggle is a fresh intent, not a duplicate).
* fix: drop sticky-default UX, treat disabled-selected model like any other
Removes the sticky-default carve-out so a globally-disabled model is
never shown in the Agent's Language Model dropdown, regardless of
whether it's the node's saved selection. Drops the Wrench/Configure
affordance — there's no per-trigger "this model isn't enabled for
your user" UX anymore; the dropdown is the single source of truth.
Changes in ``modelInputComponent``:
- ``groupedOptions``: drop the ``isStickyNotEnabled`` filter
bypass and the zero-provider import fallback. Disabled models
never pass the filter, even when tagged ``not_enabled_locally``.
- ``selectedModel``: drop the saved-value preservation branch.
If the saved name isn't in ``flatOptions``, fall through to the
first available option (or ``null`` if nothing is available).
- Auto-select effect: fires whenever the saved value isn't in
``flatOptions``, not just when the value is empty. This
realigns the node's stored value with the trigger so the
rendered selection and the run-time selection don't diverge.
- ``hasEnabledProviders``: redefined as "at least one model of
this component's type is enabled across any provider"
(derived from ``groupedOptions``). Routes a configured-but-
all-disabled provider to the Setup Provider CTA — same UX as
the never-configured case.
- Remove the Wrench JSX, the ``showConfigureAffordance``
derivation, and the now-orphaned ``hasProcessedEmptyRef``.
Tests:
- ``renders the Setup Provider CTA when no models are enabled``
(replaces the old disabled-combobox expectation).
- ``auto-selects an available model when the saved value is
globally disabled``.
- ``renders the Setup Provider CTA when a provider is configured
but all its models are disabled``.
- ``never renders the Configure wrench affordance``.
* refactor: extract toggle queue into useModelToggleQueue hook
Addresses review I1 (file size + mixed responsibilities) and R1, R2,
R3, R4 in one pass.
I1 — Extract toggle-queue logic out of useProviderConfiguration. The
parent hook drops from 821 lines to 601 and now owns a single
responsibility (variable CRUD + provider lifecycle). The toggle queue
— overlay/unsent buffers, debounced flush, awaitable flush, optimistic
cache management, re-overlay effect — lives in a new 292-line
useModelToggleQueue.ts with one clear responsibility.
R1 — Inside the new hook, DRY the two flush variants behind shared
helpers:
- ``buildAndConsumeToggleBatch()`` snapshots unsentToggles into the
mutation payload, captures the pre-toggle cache for rollback, and
drains unsentToggles atomically.
- ``rollbackToggleBatch()`` drains overlay BEFORE restoring
previousData (load-bearing order — otherwise the re-overlay effect
would re-apply the stale overlay over the rollback).
The two flush callers now differ only in ``mutate`` vs awaitable
``mutateAsync`` plumbing.
R2 — Explicit loop-guard comment on the drift check inside the
re-overlay effect. Names the ``drifted`` short-circuit as the
termination condition so a future refactor that "simplifies" to an
unconditional re-apply doesn't silently introduce a render loop.
R3 — Adversarial coverage for the mutation error path:
- ``rolls back to previousData when the toggle mutation fails``
- ``does not re-apply the overlay after a failed mutation drains it``
(asserts the drain-before-rollback ordering)
- ``preserves a mid-flight re-toggle when the original mutation
fails`` (asserts a user re-toggle survives the originating
mutation's failure)
R4 — ``flushPendingChanges`` now invalidates the affected queries
inline on success, instead of relying on the caller's
``refreshAllModelInputs`` for the load-bearing invalidation. The
caller's refresh remains as an additive per-node template refresh.
Test infra: moved toggle-queue tests from
``useProviderConfiguration.test.tsx`` to a dedicated
``useModelToggleQueue.test.tsx`` that exercises the extracted hook
directly. Debounce mock changed from synchronous pass-through to
explicit ``runDebounced()`` so individual tests can choose whether to
exercise the debounced path or the awaitable ``flushPendingChanges``
path.
95/95 tests pass (87 prior + new R3 coverage + R4 success-path
invalidation).
* feat: Add SSRF protection with DNS rebinding prevention
- Implement DNS pinning to prevent DNS rebinding attacks
- Enable SSRF protection by default (ssrf_protection_enabled = True)
- Add validate_and_resolve_url() function for DNS pinning
- Create SSRFProtectedTransport for custom HTTP transport
- Add comprehensive test suite for DNS rebinding protection
- Change from warn_only=True to warn_only=False for enforcement
This fixes a HIGH severity SSRF vulnerability (CVSS 8.6) that could allow
attackers to bypass SSRF protection using DNS rebinding attacks to access:
- Internal services and private networks
- Cloud metadata endpoints (AWS, GCP, Azure)
- Localhost services
The DNS pinning implementation ensures that the IP address validated during
security checks is the same IP used for the actual HTTP request, eliminating
the TOCTOU (Time-of-Check to Time-of-Use) vulnerability.
Changes:
- src/lfx/src/lfx/components/data_source/api_request.py: Integrate DNS pinning
- src/lfx/src/lfx/utils/ssrf_protection.py: Add validate_and_resolve_url()
- src/lfx/src/lfx/utils/ssrf_transport.py: Custom transport with DNS pinning
- src/lfx/src/lfx/services/settings/base.py: Enable SSRF protection by default
- src/backend/tests/unit/components/data_source/test_dns_rebinding.py: Test suite
Tested and verified working in GUI with DNS pinning logs visible.
* fix(security): implement network-level DNS pinning for SSRF protection with HTTPS support
This commit implements proper DNS rebinding protection that works with both HTTP and HTTPS by using network-level DNS pinning instead of URL rewriting.
## Changes
### Security Fixes
- **ssrf_protection.py**: Fixed critical vulnerability where IP validation returned early on first allowlisted IP, skipping validation of remaining IPs. Now validates ALL resolved IPs before making decisions.
- Blocks hostname if ANY resolved IP is blocked (even if others are allowlisted)
- Supports partial allowlisting (uses only allowlisted IPs for DNS pinning)
- Updated documentation to reflect that SSRF protection is now enabled by default
- Changed warn_only default from True to False for stricter security
### DNS Pinning Implementation
- **ssrf_transport.py**: Created DNSPinningNetworkBackend class that implements network-level DNS pinning
- Extends httpcore.AsyncNetworkBackend to intercept TCP connections
- Connects to pinned IP at network layer while preserving hostname for TLS
- Works with both HTTP and HTTPS (fixes HTTPS regression from URL rewriting approach)
- Properly handles TLS SNI and certificate verification
### Testing
- **test_dns_rebinding.py**: Updated all DNS rebinding tests to verify network-level behavior
- Tests now patch AutoBackend.connect_tcp to capture actual IP connections
- Verifies DNS pinning prevents rebinding attacks
- Verifies hostname preservation for TLS
- Tests IPv6 support
- All 7 tests passing
## Technical Details
The previous URL rewriting approach (replacing hostname with IP in URL) broke HTTPS because:
1. TLS certificate is issued for hostname, not IP address
2. Certificate verification fails when connecting to IP
3. This was a regression - HTTPS worked before but was vulnerable
The new network-level approach:
1. Resolves DNS during validation and pins IPs
2. Uses custom AsyncNetworkBackend to intercept TCP connections
3. Connects to pinned IP at network layer
4. Preserves original hostname for TLS SNI and certificate verification
5. Works transparently with both HTTP and HTTPS
## Function Clarification
Two validation functions exist for different purposes:
- validate_url_for_ssrf(): Simple validation for URL component (no HTTP requests)
- validate_and_resolve_url(): Validation + DNS pinning for API Request component (makes HTTP requests)
Fixes DNS rebinding vulnerability while maintaining HTTPS compatibility.
* fix(security): support multiple IPs for dual-stack and load-balanced hosts
This commit fixes DNS pinning to properly handle hosts that resolve to multiple
IP addresses (dual-stack IPv4/IPv6, load balancing, etc.).
## Changes
### DNS Pinning Enhancement
- **ssrf_transport.py**: Updated to accept and use list of validated IPs
- Changed pinned_ips from dict[str, str] to dict[str, list[str]]
- DNSPinningNetworkBackend now tries IPs in order with fallback
- Supports dual-stack (IPv4+IPv6) and load-balanced hosts
- Falls back to next IP if connection fails
- **api_request.py**: Pass all validated IPs instead of just first one
- Changed from validated_ips[0] to validated_ips
- Enables proper failover for unreachable IPs
### Testing
- **test_dns_rebinding.py**: Added test_dns_pinning_with_multiple_ips_fallback
- Verifies system tries multiple IPs when first fails
- Tests dual-stack scenario (IPv4 fails, IPv6 succeeds)
- Confirms IPs are tried in order
- All 8 tests passing
## Technical Details
**Previous behavior:**
- Only used first validated IP (validated_ips[0])
- Failed if that single IP was unreachable
- Broke dual-stack and load-balanced hosts
**New behavior:**
- Accepts full list of validated IPs
- Tries each IP in order until one succeeds
- Properly supports:
- Dual-stack hosts (IPv4 + IPv6)
- Load-balanced hosts (multiple IPs)
- Failover scenarios
This maintains security (all IPs validated during SSRF check) while improving
reliability and compatibility with modern networking configurations.
* fix: improve SSRF protection with DNS pinning and fix resource leak
- Implement network-level DNS pinning to prevent DNS rebinding attacks
- Fix resource leak in SSRFProtectedTransport by not calling super().__init__()
- Simplify error messages in SSRF protection (remove verbose explanations)
- Restore warn_only parameter in validate_url_for_ssrf() for backward compatibility
- Remove warn_only from validate_and_resolve_url() (not needed in API Request)
- Switch from private AutoBackend to public httpcore.AnyIOBackend() API
- Add noqa comments for unavoidable FBT and B008 warnings (matching parent class signature)
- Support dual-stack (IPv4/IPv6) and load-balanced hosts with multiple IPs
- Move imports to top of file for better code organization
* [autofix.ci] apply automated fixes
* [autofix.ci] apply automated fixes (attempt 2/3)
* test: fix SSRF protection test mocks to use validate_and_resolve_url
- Updated test mocks from validate_url_for_ssrf to validate_and_resolve_url
- Fixed test_ssrf_protection_disabled_by_default to explicitly disable protection
- All 12 SSRF protection tests now passing
* fix: read SSRF allowlist directly from environment for test compatibility
- Modified get_allowed_hosts() to read from os.getenv() first
- Fixes test isolation issue where settings service cached old values
- Ensures patch.dict() in tests properly overrides allowlist
- Maintains backward compatibility with settings service fallback
* test: fix test_ssrf_protection_enforcement_mode to properly mock validation
The test was failing because it wasn't mocking validate_and_resolve_url,
so the actual SSRF protection wasn't being triggered. Updated the test to:
- Mock validate_and_resolve_url to raise SSRFProtectionError
- Follow the same pattern as other SSRF protection tests
- Properly test that API Request component enforces blocking (no warn-only mode)
* refactor: rewrite SSRF protection tests to test real implementation
BREAKING CHANGE: Completely rewrote SSRF protection tests to properly test
actual security behavior instead of mocking core functions.
Changes:
- Removed all mocks of validate_and_resolve_url() - tests now verify real SSRF blocking
- Added tests that verify DNS pinning actually prevents rebinding attacks
- Test real blocking of private IPs (127.0.0.1, 192.168.x.x, 10.x.x.x, 172.16.x.x)
- Test real blocking of cloud metadata endpoints (169.254.169.254)
- Verify allowlist functionality with actual hostnames, IPs, and CIDR ranges
- Test that custom DNS pinning transport is used when protection is enabled
- Test that normal httpx client is used when protection is disabled
- Fixed environment variable caching in is_ssrf_protection_enabled()
Why this matters:
The old tests were mocking validate_and_resolve_url(), which meant they weren't
actually testing if SSRF protection works. They were just testing error handling.
Real SSRF vulnerabilities could have slipped through.
The new tests:
1. Let the real SSRF protection code run
2. Verify actual private IPs are blocked
3. Verify public URLs are allowed
4. Verify allowlist bypasses work correctly
5. Verify DNS pinning transport is actually used
All 41 tests pass (37 passed, 4 skipped for version checks).
---------
Co-authored-by: Janardan S Kavia <janardanskavia@Janardans-MacBook-Pro.local>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
* chore: import AGENTS.md from CLAUDE.md so Claude Code loads project instructions
Claude Code does not yet natively support AGENTS.md. The previous CLAUDE.md
was a human-readable redirect, but the agent never followed it. Using the
@AGENTS.md import directive (which Claude Code does support) makes the
project instructions auto-load.
* chore: clarify dev-deps sync and pre-commit workflow in AGENTS.md
- Note that running tests inside a sub-package needs uv sync --group dev
--package <name>; the default sync skips sub-package dev deps and leaves
things like fakeredis uninstalled.
- Tighten the pre-commit workflow section: pre-commit hooks already run
ruff/biome on commit, so the previous 5-step manual sequence overstated
what's needed.
- Update builder stage: bookworm-slim → trixie-slim (5 Dockerfiles)
- Update runtime stage: python:3.12.13-slim-trixie → python:3.12-slim-trixie
- Add pull: true to 6 Docker build steps in nightly workflow
- Forces pulling latest base images instead of using cached layers
This resolves CVE vulnerabilities in Docker images by ensuring we use
the latest Debian Trixie base images instead of cached Bookworm layers.
Adds a validate-pr-title job to ci.yml that fails on PRs whose title
ends with '...' or '…'. Wired into the CI Success aggregate so it acts
as a merge gate. Skipped on merge_group, workflow_dispatch, and
workflow_call where no PR title is available.
* docs: Add DESIGN.md specification for Langflow's visual design system
Machine-readable design tokens (YAML frontmatter) paired with
human-readable design rationale following the open-source DESIGN.md
format from Google Labs. Intended for AI agents generating UI that
must match Langflow's visual identity.
Covers 129 color tokens, 15 typography scales, 50 component
definitions, spacing/rounding/elevation systems, dark mode strategy,
and the 14-hue data type color system used for node port encoding.
* ci: Add DESIGN.md lint workflow
Runs `@google/design.md lint` against DESIGN.md on pull requests that
modify the file. Catches spec violations, broken token references, and
WCAG AA contrast failures before merge. CLI version is pinned via env
var so format spec changes require a deliberate bump.
* ci: Integrate DESIGN.md lint into CI Success pipeline
Replaces the standalone workflow with a job inside ci.yml so it gates
the "CI Success" aggregate check. Path-filter skips the job on PRs
that don't touch DESIGN.md, so cost is near-zero for normal PRs.
Adds release-.* to reviews.auto_review.base_branches so PRs targeting
release branches (e.g. release-1.9.2) receive the same auto-review as
PRs targeting main.
These files were superseded by docker/frontend/ in Dec 2024 but left in
place. They still use the pre-openshift sed -i entrypoint that breaks
under readOnlyRootFilesystem, so any local build from this Dockerfile
produces a broken image. Nothing in CI, Makefile, or build tooling
references them.
chore: update playwright to 1.59.0
- Update playwright dependency from 1.58.0 to 1.59.0
- Sync uv.lock with updated dependencies
Co-authored-by: Janardan S Kavia <janardanskavia@Janardans-MacBook-Pro.local>
`BaseFileComponent._unpack_bundle._safe_extract_tar` accepted any TAR member
type and only checked that `output_dir / member.name` did not escape the
extract dir. That check was performed before extraction, so a symlink whose
*target* was an absolute path (or `../` escape) was extracted untouched.
Once on disk the link was iterated by `temp_dir_path.iterdir()` and handed
to `process_files()`, whose concrete implementations (FileComponent,
DoclingInline/Remote, NvidiaIngest, VideoFile, Unstructured) call
`path.read_bytes()` and follow the link to read arbitrary host files.
The reporter's exploit chain leaks `~/.langflow/secret_key`, forges a JWT
for an admin user, and then runs arbitrary code through the Python
interpreter node, achieving RCE.
Python's `tarfile` only defaults to the safe `data` filter on Python 3.14,
which langflow's `requires-python = ">=3.10,<3.14"` excludes — so every
supported interpreter was vulnerable.
Fix:
- `_safe_extract_tar` now rejects symbolic-link, hard-link, FIFO, and
device-node members with a `ValueError` and only extracts regular files
and directories.
- `_unpack_and_collect_files` skips any `is_symlink()` entries from the
extracted bundle directory and from recursive directory walks as
defense-in-depth in case a future bundle format slips a link through.
- New `tests/unit/base/data/test_base_file_unpack.py` covers symlink (abs
+ relative escape), hardlink, FIFO rejection, benign tar/zip extraction,
the post-extraction symlink filter, and an end-to-end repro mirroring
the advisory PoC (real filesystem symlink → tarfile.add).
Refs: https://github.com/langflow-ai/langflow/security/advisories/GHSA-ccv6-r384-xp75
* fix: namespace inputs.session on build_public_tmp
The unauthenticated POST /api/v1/build_public_tmp/{flow_id}/flow endpoint
accepted a caller-supplied inputs.session that was forwarded to the build's
Memory component verbatim, letting an attacker read chat history stored
under any session id, including the predictable session_id == flow_id that
/api/v1/run hands out by default.
This is a follow-up to the prior CVE-2026-33017 fix (#12160), which removed
the attacker-controlled data parameter but left inputs.session accepted.
Wrap any non-None inputs.session under the per-(client_id, flow_id) virtual
flow ID via a new scope_session_to_namespace helper. Empty strings,
in-namespace values, and pre-prefixed values are handled idempotently. None
passes through unchanged; downstream falls back to the virtual flow ID.
Adds @pytest.mark.security regression tests including an end-to-end memory
query collision check.
* chore: wrap over-length --session-id help string
Fixes pre-existing E501 in _running_commands.py:67 introduced by #12906.
Splits the help string across two lines to fit the 120-column limit.
* fix(lfx): unblock streaming flows in lfx run
LCModelComponent._handle_stream tried to persist a partial Message via
send_message whenever the LM was wired to ChatOutput, but the message-
store path requires session_id and the chunk-consumption path requires
an EventManager. lfx run had neither, so flows with stream=True crashed
in astore_message ("session_id, sender, sender_name must be provided").
- run_flow now auto-generates a session_id when none is supplied so the
message-store validator passes; an explicit value still wins.
- lfx run gains a --session-id flag for memory continuity across runs
(Memory / MessageHistory components keyed on session_id).
- _handle_stream now also requires an EventManager before taking the
streaming branch — without one, the chunk iterator would be stored
but never drained, surfacing as an empty result downstream. Falls
back to ainvoke and returns the full text instead.
Tests cover the autogen, caller-precedence, and uniqueness cases on
run_flow plus the four _handle_stream branches (no session_id, no
event_manager, both present, not connected to chat output).
* fix(lfx): autogen session_id in CUGA agent when graph has none
Matches the pattern already used by base/agents/agent.py and
base/agents/altk_base_agent.py. Without this, calling the CUGA agent
outside run_flow (which now autogens a session_id) would still fail
astore_message validation.
* [autofix.ci] apply automated fixes
* fix(lfx): plumb session_id through serve /run and /stream
StreamRequest already declared a session_id field but it was never
applied to the graph; RunRequest didn't have the field at all. Both
endpoints called execute_graph_with_capture, which executed against
an empty graph.session_id, so message-store paths skipped storage
silently and Memory components could not maintain continuity.
- Add session_id to RunRequest.
- Have execute_graph_with_capture accept session_id, autogen if empty
(matches run_flow), and apply it to graph.session_id before
execution.
- Forward session_id from both /run and /stream handlers.
* fix(lfx): propagate session_id and user_id so memory works on lfx run
The lfx run path uses graph.async_start instead of graph.arun, bypassing
the has_session_id_vertices propagation loop in Graph._run that the
playground hits via build_graph_from_data. Memory/MessageHistory
components reading session_id from their input field would see "" even
when --session-id was passed. Replicate the loop in run_flow and
execute_graph_with_capture, with the same precedence as the playground:
hardcoded values on the component win.
AgentComponent's variable lookup precheck blocks any flow that resolves
variables (e.g. api_key) when graph.user_id is empty. Auto-generate a
ceremonial UUID; lfx's env-fallback variable service ignores user_id, so
this only satisfies the precheck — env vars remain process-global with
no per-user scoping.
Make VariableService.get_variable async to match the langflow call site
(`await variable_service.get_variable(...)` in custom_component.get_variable).
The signature accepts and ignores user_id/field/session kwargs so flows
behave identically under either backend.
Tests cover propagation precedence (empty input filled, hardcoded value
preserved, missing vertex skipped), user_id auto-gen and caller-takes-
precedence, and the async signature with kwarg absorption.
* [autofix.ci] apply automated fixes
* [autofix.ci] apply automated fixes (attempt 2/3)
* [autofix.ci] apply automated fixes (attempt 3/3)
* fix(lfx): plumb fallback_to_env_vars so DatabaseVariableService works in lfx run
The lfx run path uses graph.async_start, which never propagated
fallback_to_env_vars to vertex builds — the flag defaulted to False all
the way down to update_params_with_load_from_db_fields. Result: if a user
swapped lfx's env-fallback VariableService for langflow's
DatabaseVariableService (via lfx.toml), every load_from_db variable
(e.g. api_key=OPENAI_API_KEY) would raise "variable not found" because
the random ceremonial user_id has no DB rows, with no env fallback.
Add fallback_to_env_vars kwarg to async_start and astep (default False,
non-breaking for existing callers). run_flow and execute_graph_with_capture
read settings.fallback_to_env_var (defaults True, settable via
LANGFLOW_FALLBACK_TO_ENV_VAR=false) and pass it through. Mirrors what
processing.process.run_graph_internal does for the langflow API path.
Make memory.stubs.astore_message tolerant of non-UUID flow_ids: the stub
is the no-op fallback when no real database is registered, so it should
not crash on synthetic identifiers (e.g. test fixtures, lfx callers
passing string flow ids). UUID parsing only normalizes format; an
invalid string is preserved verbatim.
Tests:
- TestRunFlowFallbackToEnvVars: confirms run_flow forwards
fallback_to_env_vars from settings (default and disabled).
- TestGraphExecution: same for execute_graph_with_capture.
- Existing mock_async_start signatures updated to accept **kwargs.
- Test fixtures using flow_id="test-flow-id" replaced with a real UUID
so the now-active ChatInput storage path doesn't trip stubs.py's
UUID parse — aligning fixtures with production semantics.
* test(lfx): pass session_id=None when invoking the typer run() directly
The lfx test suite calls ``run(...)`` (the typer command) without going through
typer's CLI parser. Any parameter with a ``typer.Option(...)`` default in the
signature (e.g. ``session_id``) evaluates to a ``typer.models.OptionInfo``
sentinel under that invocation pattern, not None. The session_id propagation
loop then writes that sentinel into ``vertex.raw_params["session_id"]``, which
fails ``MessageTextInput`` validation with
``Invalid value type <class 'typer.models.OptionInfo'>``.
Fix at the call site: pass ``session_id=None`` explicitly. Mirrors how typer
would resolve the option after parsing CLI args. Two tests affected;
test_run_command.py now reflects the constraint that calls bypassing typer
must pass all option-shaped args.
* fix(lfx): harden session_id/user_id handling on lfx run path
Addresses review findings on the streaming-fix PR:
- Reject empty/whitespace --session-id and --user-id up-front so a shell
quirk or empty env var surfaces a clear error instead of silently
auto-generating a fresh session and breaking Memory continuity.
- Extract a shared helper (lfx/run/_defaults.py) for session_id/user_id
auto-gen, vertex propagation, and fallback_to_env_vars resolution; both
run_flow and execute_graph_with_capture now delegate to it.
- Warn when settings_service is None (silently flipped fallback_to_env_vars
to False before).
- CUGA agent: wrap uuid.uuid4() with str() to match the rest of the file
and the codebase's expectation that Message.session_id renders as a hex
string. Add focused tests with module-level skip when the cuga import
side-effect (MODELS_METADATA["OpenAI"]) isn't satisfiable.
- Streaming-fallback warning now names the component (display_name + _id)
so users can identify which model fell back to ainvoke.
- memory/stubs.py: replace contextlib.suppress(ValueError) with explicit
try/except + warning so malformed flow_ids leave a breadcrumb.
- Improve --session-id help text to explain WHEN to set it.
- Tune session_id auto-gen log level from warning to info (less noisy on
default CLI runs; visible at -v).
- Add tests: --session-id CLI plumbing, multi-call continuity, empty/
whitespace rejection, and isinstance(str) on the streaming fallback path.
* [autofix.ci] apply automated fixes
---------
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
* fix(lfx): allow env-var fallback when user_id is None
get_api_key_for_provider short-circuited to None whenever user_id was
None, making the os.getenv(variable_name) fallback at the bottom of the
function unreachable. lfx run has no concept of user_id, so any flow
exported with an empty credential field (the typical case after a
re-export) failed with 'API key is required' even when the canonical
env var was set in the shell.
Restructured to: try the database-backed variable service only when a
user_id is available, then always fall through to os.getenv. The
shell-exported credential is now picked up regardless of whether a
user is present.
* ruff
:
g
* feat: Add telemetry to deployments API
Instruments 8 CUD-shaped deployment routes to emit telemetry events for tracking usage, duration, and error rates.
Changes:
- `schema.py`: Added `DeploymentPayload` Pydantic model to define the structure of telemetry data for deployment events (action, provider, seconds, success, error_type).
- `service.py`: Added three async methods (`log_package_deployment`, `log_package_deployment_provider`, `log_package_deployment_run`) to enqueue deployment events to the telemetry queue.
- `deployments.py`:
- Created `DeploymentTelemetryCtx` dataclass and a non-invasive yield-based FastAPI dependency (`_make_telemetry_dep`) to handle timing, success/failure detection, and event emission.
- Instrumented 8 routes (create/update/delete for deployments and providers, create run, update snapshot) by injecting the telemetry dependency and setting the provider context.
- `test_telemetry_schema.py`: Added unit tests for `DeploymentPayload` initialization, defaults, serialization, and roundtrip.
- `test_telemetry.py`: Added unit tests for the new `log_package_deployment*` service methods, including a check for the `do_not_track` setting.
- `test_deployments_telemetry.py`: Created a new integration test file with 10 tests covering happy paths, error paths (e.g., `HTTPException` mapping), and cross-route smoke tests for all instrumented endpoints.
* get rid of dup noqas
* update deprecated status http
* update unit tests
* feat: capture wxo_tenant_id in deployments telemetry
Adds `wxo_tenant_id` to DeploymentPayload (alias `wxoTenantId`) and
`DeploymentTelemetryCtx` so provider-account tenant identity is recorded
alongside provider/action/duration. Threads `provider_tenant_id` through
`resolve_adapter_from_deployment` and `resolve_adapter_mapper_from_deployment`
return tuples so all 8 instrumented routes populate the field, not just the
5 that already had `provider_account` in scope.
* Change exception type to error message to match existing patterns
---------
Co-authored-by: Hamza Rashid <hzarashid@gmail.com>
* chore: security patch
security patch
* chore: upgrade package-lock.json
* chore: smolagents and transformer update
* chore: redis upgrade
* chore: litellm upgrade
* fix: Pin click to avoid lower versions
## Root cause
**litellm 1.83.5+** introduced an exact pin `click==8.1.8` in its `requires_dist` (upstream bug [BerriAI/litellm#26154](https://github.com/BerriAI/litellm/issues/26154) — "Pinning exact dependency versions breaks downstream consumers"). When this branch bumped litellm to 1.83.11, uv was forced to downgrade click from 8.3.2 → 8.1.8. Click 8.2+ captures stderr separately by default; 8.1 merges it — hence `ValueError: stderr not separately captured`.
## Fix
- [pyproject.toml:155](pyproject.toml:155) — added `"click>=8.3.0"` to `[tool.uv] override-dependencies` with comment linking the upstream issue.
- [src/backend/base/pyproject.toml:112](src/backend/base/pyproject.toml:112) — same override so `langflow-base` Docker builds pick it up too.
- Regenerated `uv.lock` (click: 8.1.8 → 8.3.3).
- Patched the `click` entry in `src/backend/base/uv.lock` to 8.3.3 (that lockfile is used by `docker/build_and_push_base.Dockerfile` via `uv sync --frozen`; uv treats backend/base as a workspace member so I updated its click block directly — no other references to 8.1.8 remain).
- Reverted [src/lfx/tests/unit/cli/test_run_real_flows.py](src/lfx/tests/unit/cli/test_run_real_flows.py) back to its original `result.stderr` usage — now passes cleanly with click 8.3.3.
All 19 tests in the file pass.
* Update build_and_push_backend.Dockerfile
The `uv pip install` command doesn't read `[tool.uv] override-dependencies` from the target packages' pyproject.toml — those overrides only apply during `uv lock`/`uv sync`. The fix is to pass the overrides explicitly via `--overrides`.
Now let me verify it works by running the same install locally.
Resolution succeeded — click==8.3.3, aiohttp==3.13.5, litellm==1.83.11 all coexist. Let me also test without `--overrides` to confirm I reproduce the original failure.
Reproduced the Docker failure locally. Now test with the overrides file:
Fix verified — resolution succeeds with the overrides file, failing without. Let me clean up and show the final Dockerfile change.
## Summary
**Root cause:** The Dockerfile uses `uv pip install ./src/...` which **does not read** `[tool.uv] override-dependencies` from the target packages' `pyproject.toml` — those overrides only apply during `uv lock`/`uv sync`. On Linux, `cuga>=0.2.20` requires `aiohttp>=3.13.5`, but `litellm==1.83.11` exact-pins `aiohttp==3.13.3` (same upstream bug as the click pin, [BerriAI/litellm#26154](https://github.com/BerriAI/litellm/issues/26154)). Without an active override, resolution fails.
**Fix:** Write a `/tmp/uv-overrides.txt` file mirroring the workspace's `override-dependencies` (litellm, python-dotenv, openai, aiohttp, click) and pass `--overrides /tmp/uv-overrides.txt` to `uv pip install`.
Verified locally by reproducing the exact failure in an isolated tmp directory (to escape the workspace's pyproject.toml auto-apply), then confirming the overrides file resolves it:
- aiohttp==3.13.5 ✓
- click==8.3.3 ✓
- litellm==1.83.11 ✓
- cuga==0.2.22 ✓
* Templates version update
* Update .secrets.baseline
* chore: update overrides
* chore: bump version
* chore: bump main version
* chore: bump SDK version to 0.1.1
* chore: run uv lock and uv sync after SDK version bump
* chore: read the overrides from a single source
security patch
* chore: add pip check toggle
add pip check toggle
* chore: litellm base uv.lock update
* fix(chore): Pin litellm back to last working release
* fix(chore): Pin to 1.83 and higher litellm
* Update build_and_push_backend.Dockerfile
* chore: update base uv.lock
* fix: lazyload toolguard since its an optional extra
* Update .secrets.baseline
* Update component_index.json
* Rebuild component index
* Update component_index.json
---------
Co-authored-by: Eric Hare <ericrhare@gmail.com>
Co-authored-by: vijay kumar katuri <vijay.katuri@ibm.com>
(cherry picked from commit 60a8f76c3fde17124057626c671ea8162872837a)
Backend and frontend images (langflowai/langflow-backend and
langflowai/langflow-frontend) are separate from the main image
(langflowai/langflow), so they should not be skipped when the
main version already exists on Docker Hub.
This fixes the issue where backend/frontend builds for 1.9.0
were skipped because the main 1.9.0 image was already published,
preventing these critical images from being available on Docker Hub.
Fixes: Backend and frontend Docker images not published in 1.9.0 release
(cherry picked from commit 595587027d)
fix(lfx): route memory ops to stubs when DB service is noop
lfx.memory bound to langflow.memory at import time whenever the langflow
package was importable, even when the registered DB service was
NoopDatabaseService. langflow.memory.aupdate_messages then called
session.get() on a NoopSession (which unconditionally returns None) and
raised spurious "Message with id X not found" errors mid-stream from the
Agent component.
Dispatch now happens at call time via has_langflow_db_backend(), which
requires both langflow to be importable AND a non-noop DB service to be
registered. Call-time evaluation is required because the DB service is
typically registered after lfx.memory is first imported during component
class loading.
(cherry picked from commit 6453d66de6)
* fix(security): default WEBHOOK_AUTH_ENABLE to True (unauth webhook execution)
POST /api/v1/webhook/{flow_id} previously executed any user's flow
without authentication because WEBHOOK_AUTH_ENABLE defaulted to False.
Change the default to True so webhook endpoints require an API key and
validate that the caller owns the flow being executed. Operators who
need the prior behavior can explicitly opt in with
LANGFLOW_WEBHOOK_AUTH_ENABLE=False.
Docs updated to reflect the new secure-by-default behavior.
* test(security): add regression tests for WEBHOOK_AUTH_ENABLE default
Guard against a regression of the unauthenticated webhook execution fix:
one test pins the class-level default to True, the other confirms the
runtime config rejects an unauthenticated POST with 403 under defaults.
(cherry picked from commit cb6f7508dd)
- Add tag format validation (must start with 'v')
- Check for duplicate tags without 'v' prefix
- Prevent release notes from using wrong base comparison
- Add validate-tag-format job dependency to create_release
Fixes tag duplication issue that caused v1.9.0 release notes to miss 58 commits.
Root cause: Duplicate tags (1.8.3 vs v1.8.3) caused GitHub's generateReleaseNotes
to pick the wrong base tag due to alphabetical sorting.
This ensures future releases will:
1. Only accept tags with 'v' prefix (v1.2.3 format)
2. Detect and reject releases if duplicate tags exist
3. Generate correct release notes with proper commit history
(cherry picked from commit a754961428)
* Set wxo ff to true by default
* update test
* [autofix.ci] apply automated fixes
---------
Co-authored-by: Hamza Rashid <74062092+HzaRashid@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
(cherry picked from commit 0b5038cf41)
* fix(security): close IDOR in get_flow_by_id_or_endpoint_name (LE-639)
The helper at helpers/flow.py::get_flow_by_id_or_endpoint_name had two
symmetric holes that let an authenticated user resolve another user's
flow by UUID or endpoint_name:
1. UUID branch called ``session.get(Flow, flow_id)`` with zero ownership
check -- user_id was completely ignored when a UUID was supplied.
2. endpoint_name branch only applied the user_id filter when a truthy
user_id was passed. FastAPI ``Depends(get_flow_by_id_or_endpoint_name)``
resolves user_id as a query parameter that no real caller sets, so the
filter was silently skipped on every route using the Depends pattern.
Reported exploit path (Jira LE-639): POST /api/v1/responses with the
victim's flow UUID in the ``model`` field executes the victim's flow
and returns the output. openai_responses.py and v2/workflow.py pass
user_id explicitly, so fixing the helper auto-fixes those endpoints;
the v1 /run* endpoints are defense-in-depth covered by
check_flow_user_permission in _run_flow_internal, but now fail closed
at the helper layer too.
Fix normalizes user_id once at the top of the helper and enforces it on
both branches. Cross-user lookups return None so the shared 404 path
fires (matches api/v1/files.py::get_flow pattern), which avoids
disclosing flow existence via a 403-vs-404 oracle. user_id=None
preserves the existing no-scope behavior for webhooks and internal
callers that legitimately need cross-user lookup.
Tests:
- 8 new unit tests on the helper covering same-user UUID, cross-user
UUID (primary IDOR), UUID instance vs str user_id, no-user-scope,
missing-flow, endpoint_name with and without user_id.
- 1 new integration test reproducing the Jira PoC on /api/v1/responses:
attacker with valid API key receives flow_not_found when supplying
the victim's flow UUID.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(security): apply LE-639 scoping to the /run* Depends routes too
Reviewer pointed out two gaps in the original helper-only fix:
1. Three /run* endpoints still used ``Depends(get_flow_by_id_or_endpoint_name)``
directly. FastAPI exposes ``user_id`` as a plain query parameter in that
shape, which no real caller sets, so the helper stayed unscoped on those
routes and ownership was only enforced later by check_flow_user_permission
with a 403. That leaves a 403-vs-404 flow-existence oracle open.
2. Eager ``UUID(user_id)`` normalization in the helper turned a malformed
query-string ``?user_id=foo`` into a raw 500.
Fixes:
- Add two auth-aware wrapper dependencies in endpoints.py:
- get_flow_for_api_key_user: pulls the caller from api_key_security
- get_flow_for_current_user: pulls the caller from CurrentActiveUser
Both forward the authenticated user_id to the existing helper.
- Swap the three bare dependencies to the wrappers:
- /api/v1/run/{flow_id_or_name}
- /api/v1/run/session/{flow_id_or_name}
- /api/v1/run/advanced/{flow_id_or_name}
The webhook and webhook-events routes keep the bare helper: webhook is
intentionally public and webhook-events already does an explicit
str(flow.user_id) != str(user.id) check before subscribing.
- In helpers/flow.py, wrap the UUID(user_id) normalization and convert
ValueError / AttributeError into the same 404 path used for missing
flows. Keeps the fail-closed posture -- a caller whose identity we
can't resolve never learns whether a flow exists.
Tests:
- Update the four existing cross-user tests (/run, /run with payload,
/run with streaming, /run/advanced) from 403 to 404, and swap the
"You do not have permission" substring check for a leak-safe
"'permission' not in response.text.lower()" assertion. Also update
test_permission_check_blocks_before_execution for the same reason.
- Add test_user_cannot_run_other_users_flow_session_endpoint for the
/run/session path (monkeypatches agentic_experience=True and logs in
as a second local user via session auth).
- Add test_run_rejects_malformed_user_id_query_param asserting that
``?user_id=<junk>`` never surfaces as 500 on the /run route.
- Add three parametrized unit tests on the helper covering "not-a-uuid",
"", and "12345-not-a-real-uuid" -- each must raise 404 without hitting
the database.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit 2c9f498d66)