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