SIGN IN SIGN UP

fix(security): scope storage-key resolution to the executing graph's namespace (#14645)

* fix(security): scope storage-key resolution to the executing graph's namespace

File-reading components accept a path from a tenant-controlled input. When that
value has the shape "<namespace>/<file_name>" it is not treated as a filesystem
path but as a storage key: the namespace segment selects a per-user / per-flow
directory under config_dir (local storage) or object prefix (S3). Nothing checked
that the namespace belonged to the graph that was executing, so the shape of the
path was effectively making the access decision and a caller could address another
principal's uploads.

Adds enforce_storage_key_scope() in lfx.utils.file_path_security and applies it at
both places a storage key is turned into a real location:

1. ParameterHandler.process_file_value - the primary route. FileInput values from
   the saved template or a runtime tweak are expanded by
   StorageService.resolve_component_path() before the component runs, so the
   namespace must be checked before resolution; afterwards the component only sees
   an already-resolved path. Applied to relative keys only, and only when
   LANGFLOW_RESTRICT_LOCAL_FILE_ACCESS is off - restricted mode already pins the
   resolved path and the S3 logical key to the graph's scopes, so its behavior is
   unchanged. Absolute paths stay with the existing flag-gated containment, which
   preserves single-tenant "read any local file" support.

2. CustomComponent.get_full_path and the S3 object-key branch of
   BaseFileComponent._validate_and_resolve_paths - the routes where the component
   resolves a key itself (Data inputs from another component, tool-mode strings,
   scripted graphs). Unconditional here: addressing another principal's namespace
   is never the single-tenant local-file feature the flag governs.

The contract in both places: the namespace must be one of the executing graph's own
scopes (authenticated user id, executing flow id, trusted public source flow id -
the same set enforce_local_file_access already uses), and the file name must be a
single path segment so an in-scope namespace cannot be combined with a traversal to
climb back out. Graphs with no user and no flow scope (standalone `lfx run`,
scripted graphs) have no tenant boundary and keep the previous behavior.

Both component call sites that previously fell back to resolve_path on any
ValueError now re-raise StorageNamespaceError, so an access denial is never retried
as a plain local-path read. StorageNamespaceError subclasses LocalFileAccessError so
existing handlers (e.g. the build API's 400 mapping) cover it.
S3StorageService.build_full_path validates its own identifiers as defense in depth,
matching every other operation on that backend.

Regression tests cover the parameter-boundary route against a real
LocalStorageService at the OSS default, the component-level route on local and S3
storage, per-item checking of list values, traversal out of an owned namespace,
traversal to a server-managed secret, the no-fallback contract, and the legitimate
own-namespace, executing-flow-namespace and absolute-local-path cases.

Fixes LE-2237

* fix(security): never downgrade a storage-namespace denial

StorageNamespaceError subclasses ValueError, so process_file_value's broad
handler decided its fate by substring-matching an unrelated message. That is
correct today only because the denial text happens not to contain 'too many
values to unpack' -- one added branch, or one reworded message, away from
turning a cross-tenant denial back into a successful read.

Re-raise it explicitly ahead of that handler, and pin the behaviour with a test
that deliberately engineers the message collision. Without the re-raise that
test reports DID NOT RAISE, i.e. the read proceeds.

* fix: close the unterminated flow_validation import in endpoints.py

#14646 expanded the single-symbol `from lfx.utils.flow_validation import
CustomComponentValidationError` into a parenthesized block but never closed
it, so the following `from lfx.workflow.end_user_identity import (` was
swallowed into the same statement and the module stopped parsing.

The whole file is unimportable as a result: `src/backend/tests/conftest.py`
imports `langflow.main`, which reaches `langflow.api.v1.endpoints`, so every
backend pytest job dies at collection and both ruff jobs report
`invalid-syntax: Expected ')', found newline`.

All three imported symbols exist in `lfx.utils.flow_validation` and are used
in this file, so the fix is only the missing closing paren.
E
Eric Hare committed
975b2d6c1f381089667e0b39fa57ae3abdd52ae2
Parent: d6967f6
Committed by GitHub <noreply@github.com> on 8/19/2026, 5:33:26 PM