fix: prevent path traversal in LocalStorageProvider (CWE-22)#2087
Open
sebastiondev wants to merge 1 commit intolangbot-app:masterfrom
Open
fix: prevent path traversal in LocalStorageProvider (CWE-22)#2087sebastiondev wants to merge 1 commit intolangbot-app:masterfrom
sebastiondev wants to merge 1 commit intolangbot-app:masterfrom
Conversation
Add _safe_resolve() helper that uses os.path.realpath() to canonicalize the joined path and verifies it stays within LOCAL_STORAGE_PATH. All six public methods (save, load, exists, delete, size, delete_dir_recursive) now validate the key before performing any I/O. This prevents absolute-path injection (e.g. key="/etc/passwd") and relative traversal (e.g. key="../../etc/passwd") from escaping the storage root directory. CWE-22
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Vulnerability Summary
CWE-22 — Path Traversal | Severity: Critical | CVSS ~9.3 (unauthenticated RCE-adjacent)
LocalStorageProviderinsrc/langbot/pkg/storage/providers/localstorage.pyconstructs file paths via:When
keyis an absolute path (e.g."/etc/passwd"),os.path.join()discards all preceding components and returns the absolute path directly. Whenkeycontains..sequences, the path resolves outside the storage root. This enables arbitrary file read, write, and delete on the host filesystem.Data Flow
The primary attack vector is the WebSocket endpoint at
/api/v1/pipelines/<pipeline_uuid>/ws/connect, which:@self.quart_app.websocket()directly, bypassing all auth middlewareImagemessage components with an attacker-controlledpathfieldpathdirectly tostorage_provider.load()→ reads the fileos.remove()on the same path → deletes the fileNo authentication is required. The server binds
0.0.0.0with CORSallow_origin="*".All Callers of
storage_providerMethodskeywebsocket_adapter._process_image_components()component["path"]from unauthenticated WebSocketfiles.pyGET/image/<path:image_key>..check, but misses absolute paths)plugin/handler.pyGET_CONFIG_FILEdata["file_key"]from plugin IPCkbmgr.pyfile.file_namefrom DB recordsrag/service/runtime.pynormpath+..validationdelete_dir_recursivecallersFix Description
Added a
_safe_resolve(base, key)function at the storage provider layer that:baseandkeywithos.path.join()os.path.realpath()(resolves symlinks and..)os.path.realpath(base) + os.sepValueErrorif the path escapes the storage rootEvery method on
LocalStorageProvider(save,load,exists,delete,size,delete_dir_recursive) now calls_safe_resolve()before performing any filesystem operation.Rationale
os.path.realpath()+ prefix check is the standard mitigation for CWE-22 in Python (recommended by OWASP, used by Django/Flask internally)."image_abc.png"and"bot_log_images/img_001.png"continue to work normally.Changes
Test Results
9 new unit tests covering all attack vectors:
test_absolute_path_save_rejectedsave()test_absolute_path_load_rejectedload()test_absolute_path_exists_rejectedexists()test_absolute_path_delete_rejecteddelete()test_absolute_path_size_rejectedsize()test_dot_dot_path_traversal_rejected../relative traversal inload()test_delete_dir_recursive_traversal_rejecteddelete_dir_recursive()test_legitimate_key_workstest_legitimate_subdirectory_key_worksAll tests use
tmp_pathfixtures and mockLOCAL_STORAGE_PATH, so they are safe, isolated, and reproducible.Disprove Analysis
We systematically attempted to disprove this finding:
Auth Check
The WebSocket endpoint uses
@self.quart_app.websocket()directly, bypassing all auth middleware (does not useself.route()which enforces auth). TheGET /api/v1/files/image/<path:image_key>endpoint is explicitlyauth_type=group.AuthType.NONE. No authentication protects the exploitable paths.Network Check
0.0.0.0(all interfaces)allow_origin="*"Deployment Context
Dockerfile runs
uv run main.pydirectly. No reverse proxy, VPN, or service mesh configured. The README describes it as a "production-grade platform" intended to be internet-facing.Existing Mitigations Found
files.pyGET endpoint has a partial..and\\check — but misses absolute pathsrag/service/runtime.pyhas its ownnormpathvalidation — only covers that one caller<path:>converter normalises URLs — mitigates some HTTP-based attacks onlyInput Validation
Zero input validation exists in
localstorage.pybefore this fix. Thewebsocket_adapter._process_image_components()also has zero validation oncomponent["path"].Prior Reports
No prior security issues found via
gh issue list. NoSECURITY.mdexists. No recent commits to this file address security.Fix Adequacy
The fix adds
_safe_resolve()at the storage provider layer, meaning it protects all callers simultaneously. This is the correct architectural location — defense-in-depth at the lowest I/O layer. No parallel code path bypasses this fix.Verdict
CONFIRMED_VALID — High confidence. The vulnerability is unambiguously exploitable through the unauthenticated WebSocket endpoint. The
os.path.join("data/storage", "/etc/passwd")→"/etc/passwd"behavior is trivially reproducible.Preconditions for Exploitation
LocalStorageProvider(default for non-S3 deployments)*, binds0.0.0.0