Skip to content

Commit 9ed5e7c

Browse files
committed
fix(autogen-ext): restrict unpickling of task-centric memory files
1 parent 027ecf0 commit 9ed5e7c

4 files changed

Lines changed: 96 additions & 2 deletions

File tree

python/packages/autogen-ext/src/autogen_ext/experimental/task_centric_memory/_memory_bank.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from ._string_similarity_map import StringSimilarityMap
77
from .utils.page_logger import PageLogger
8+
from .utils.restricted_pickle import BASE_ALLOWED_PICKLE_GLOBALS, restricted_pickle_load
89

910

1011
@dataclass
@@ -79,7 +80,10 @@ def __init__(
7980
if (not reset) and os.path.exists(self.path_to_dict):
8081
self.logger.info("\nLOADING MEMOS FROM DISK at {}".format(self.path_to_dict))
8182
with open(self.path_to_dict, "rb") as f:
82-
self.uid_memo_dict = pickle.load(f)
83+
allowed = BASE_ALLOWED_PICKLE_GLOBALS | {
84+
("autogen_ext.experimental.task_centric_memory._memory_bank", "Memo"),
85+
}
86+
self.uid_memo_dict = restricted_pickle_load(f, allowed_globals=allowed)
8387
self.last_memo_id = len(self.uid_memo_dict)
8488
self.logger.info("\n{} MEMOS LOADED".format(len(self.uid_memo_dict)))
8589

python/packages/autogen-ext/src/autogen_ext/experimental/task_centric_memory/_string_similarity_map.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from chromadb.config import Settings
1010

1111
from .utils.page_logger import PageLogger
12+
from .utils.restricted_pickle import BASE_ALLOWED_PICKLE_GLOBALS, restricted_pickle_load
1213

1314

1415
class StringSimilarityMap:
@@ -45,7 +46,7 @@ def __init__(self, reset: bool, path_to_db_dir: str, logger: PageLogger | None =
4546
if (not reset) and os.path.exists(self.path_to_dict):
4647
self.logger.debug("\nLOADING STRING SIMILARITY MAP FROM DISK at {}".format(self.path_to_dict))
4748
with open(self.path_to_dict, "rb") as f:
48-
self.uid_text_dict = pickle.load(f)
49+
self.uid_text_dict = restricted_pickle_load(f, allowed_globals=BASE_ALLOWED_PICKLE_GLOBALS)
4950
self.last_string_pair_id = len(self.uid_text_dict)
5051
if len(self.uid_text_dict) > 0:
5152
self.logger.debug("\n{} STRING PAIRS LOADED".format(len(self.uid_text_dict)))
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from __future__ import annotations
2+
3+
import pickle
4+
from typing import Any, BinaryIO
5+
6+
# NOTE: The task-centric memory feature persists local state to disk. These files
7+
# can be moved between projects or restored from shared storage, so loads should
8+
# not execute arbitrary pickle globals.
9+
10+
BASE_ALLOWED_PICKLE_GLOBALS: set[tuple[str, str]] = {
11+
("builtins", "dict"),
12+
("builtins", "list"),
13+
("builtins", "set"),
14+
("builtins", "tuple"),
15+
("builtins", "str"),
16+
("builtins", "bytes"),
17+
("builtins", "bytearray"),
18+
("builtins", "int"),
19+
("builtins", "float"),
20+
("builtins", "bool"),
21+
}
22+
23+
24+
class RestrictedUnpickler(pickle.Unpickler):
25+
def __init__(self, file: BinaryIO, allowed_globals: set[tuple[str, str]]) -> None:
26+
super().__init__(file)
27+
self._allowed_globals = allowed_globals
28+
29+
def find_class(self, module: str, name: str): # noqa: ANN001
30+
if (module, name) in self._allowed_globals:
31+
return super().find_class(module, name)
32+
raise pickle.UnpicklingError(
33+
f"Blocked global during unpickle: {module}.{name}. "
34+
"Delete the persisted memory files or re-run with reset=True."
35+
)
36+
37+
38+
def restricted_pickle_load(file: BinaryIO, *, allowed_globals: set[tuple[str, str]]) -> Any:
39+
return RestrictedUnpickler(file, allowed_globals).load()
40+
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import io
2+
import os
3+
import pickle
4+
5+
import pytest
6+
7+
pytest.importorskip("chromadb")
8+
9+
10+
def test_restricted_pickle_load_blocks_unsafe_globals(monkeypatch: pytest.MonkeyPatch) -> None:
11+
from autogen_ext.experimental.task_centric_memory.utils.restricted_pickle import (
12+
BASE_ALLOWED_PICKLE_GLOBALS,
13+
restricted_pickle_load,
14+
)
15+
16+
monkeypatch.delenv("AUTOGEN_EXT_PICKLE_RCE_MARKER", raising=False)
17+
18+
class Evil:
19+
def __reduce__(self): # noqa: ANN001
20+
# Non-destructive: sets an env var if executed.
21+
return (
22+
exec,
23+
("import os; os.environ['AUTOGEN_EXT_PICKLE_RCE_MARKER']='1'",),
24+
)
25+
26+
payload = pickle.dumps(Evil())
27+
28+
with pytest.raises(pickle.UnpicklingError):
29+
restricted_pickle_load(io.BytesIO(payload), allowed_globals=BASE_ALLOWED_PICKLE_GLOBALS)
30+
31+
assert os.environ.get("AUTOGEN_EXT_PICKLE_RCE_MARKER") is None
32+
33+
34+
def test_restricted_pickle_load_allows_memo_dict_roundtrip() -> None:
35+
from autogen_ext.experimental.task_centric_memory._memory_bank import Memo
36+
from autogen_ext.experimental.task_centric_memory.utils.restricted_pickle import (
37+
BASE_ALLOWED_PICKLE_GLOBALS,
38+
restricted_pickle_load,
39+
)
40+
41+
allowed = BASE_ALLOWED_PICKLE_GLOBALS | {
42+
("autogen_ext.experimental.task_centric_memory._memory_bank", "Memo"),
43+
}
44+
45+
original = {"1": Memo(task=None, insight="hi")}
46+
payload = pickle.dumps(original)
47+
loaded = restricted_pickle_load(io.BytesIO(payload), allowed_globals=allowed)
48+
49+
assert loaded == original

0 commit comments

Comments
 (0)