CVE-2024-3571 (published 2024-04-16T00:15Z)

langchain-ai/langchain is vulnerable to path traversal due to improper limitation of a pathname to a restricted directory ('Path Traversal') in its LocalFileStore functionality. An attacker can leverage this vulnerability to read or write files anywhere on the filesystem, potentially leading to information disclosure or remote code execution. The issue lies in the handling of file paths in the mset and mget methods, where user-supplied input is not adequately sanitized, allowing directory traversal sequences to reach unintended directories.


Weakness information:
cwe_id: CWE-22
cwe_name: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
description: The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
extended_description: 
            Many file operations are intended to take place within a restricted directory. By using special elements such as ".." and "/" separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system. One of the most common special elements is the "../" sequence, which in most modern operating systems is interpreted as the parent directory of the current location. This is referred to as relative path traversal. Path traversal also covers the use of absolute pathnames such as "/usr/local/bin" to access unexpected files. This is referred to as absolute path traversal.
         
         

Fix information:
repo_url: https://github.com/langchain-ai/langchain
commit_details: repo_url: https://github.com/langchain-ai/langchain
author: Eugene Yurtsev
author_date: 2023-12-22 11:20:17-05:00
msg: langchain(patch): Restrict paths in LocalFileStore cache (#15065)

This PR restricts the paths that can be resolve using the local file system cache so that all paths must be contained within the root path.
repository: repo_url: https://github.com/langchain-ai/langchain
repo_name: langchain-ai/langchain
description: 🦜🔗 Build context-aware reasoning applications
repo_language: Jupyter Notebook
owner: langchain-ai
file_changes: filename: file_system.py
change_type: ModificationType.MODIFY
diff: @@ -1,3 +1,4 @@
+import os
 import re
 from pathlib import Path
 from typing import Iterator, List, Optional, Sequence, Tuple, Union
@@ -42,7 +43,7 @@ class LocalFileStore(ByteStore):
             root_path (Union[str, Path]): The root path of the file store. All keys are
                 interpreted as paths relative to this root.
         """
-        self.root_path = Path(root_path)
+        self.root_path = Path(root_path).absolute()
 
     def _get_full_path(self, key: str) -> Path:
         """Get the full path for a given key relative to the root path.
@@ -55,7 +56,15 @@ class LocalFileStore(ByteStore):
         """
         if not re.match(r"^[a-zA-Z0-9_.\-/]+$", key):
             raise InvalidKeyException(f"Invalid characters in key: {key}")
-        return self.root_path / key
+        full_path = os.path.abspath(self.root_path / key)
+        common_path = os.path.commonpath([str(self.root_path), full_path])
+        if common_path != str(self.root_path):
+            raise InvalidKeyException(
+                f"Invalid key: {key}. Key should be relative to the full path."
+                f"{self.root_path} vs. {common_path} and full path of {full_path}"
+            )
+
+        return Path(full_path)
 
     def mget(self, keys: Sequence[str]) -> List[Optional[bytes]]:
         """Get the values associated with the given keys.

programming_language: Python
filename: test_filesystem.py
change_type: ModificationType.MODIFY
diff: @@ -77,3 +77,26 @@ def test_yield_keys(file_store: LocalFileStore) -> None:
     # Assert that the yielded keys match the expected keys
     expected_keys = ["key1", os.path.join("subdir", "key2")]
     assert keys == expected_keys
+
+
+def test_catches_forbidden_keys(file_store: LocalFileStore) -> None:
+    """Make sure we raise exception on keys that are not allowed; e.g., absolute path"""
+    with pytest.raises(InvalidKeyException):
+        file_store.mset([("/etc", b"value1")])
+    with pytest.raises(InvalidKeyException):
+        list(file_store.yield_keys("/etc/passwd"))
+    with pytest.raises(InvalidKeyException):
+        file_store.mget(["/etc/passwd"])
+
+    # check relative paths
+    with pytest.raises(InvalidKeyException):
+        list(file_store.yield_keys(".."))
+
+    with pytest.raises(InvalidKeyException):
+        file_store.mget(["../etc/passwd"])
+
+    with pytest.raises(InvalidKeyException):
+        file_store.mset([("../etc", b"value1")])
+
+    with pytest.raises(InvalidKeyException):
+        list(file_store.yield_keys("../etc/passwd"))

programming_language: Python