Skip to content

Commit aa61689

Browse files
committed
Add audit hooks for pickle.load, pickle.loads, and pickle.Unpickler
Add sys.audit() / PySys_Audit() calls to pickle deserialization entry points, allowing security-conscious applications to monitor or block pickle deserialization via audit hooks. New audit events: - pickle.load: raised when pickle.load() is called, with the file arg - pickle.loads: raised when pickle.loads() is called, with the data arg - pickle.Unpickler: raised when Unpickler is instantiated, with file arg These are added to both the pure Python (Lib/pickle.py) and C (Modules/_pickle.c) implementations, following the same pattern as the existing pickle.find_class audit event and marshal module auditing. https://claude.ai/code/session_016rSSYy6CqG9hJZk7uzZYZ9
1 parent 149c465 commit aa61689

5 files changed

Lines changed: 51 additions & 0 deletions

File tree

Doc/library/pickle.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,8 @@ process more convenient:
243243
.. versionchanged:: 3.8
244244
The *buffers* argument was added.
245245

246+
.. audit-event:: pickle.load file pickle.load
247+
246248
.. function:: loads(data, /, *, fix_imports=True, encoding="ASCII", errors="strict", buffers=None)
247249

248250
Return the reconstituted object hierarchy of the pickled representation
@@ -258,6 +260,8 @@ process more convenient:
258260
.. versionchanged:: 3.8
259261
The *buffers* argument was added.
260262

263+
.. audit-event:: pickle.loads data pickle.loads
264+
261265

262266
The :mod:`!pickle` module defines three exceptions:
263267

@@ -433,6 +437,8 @@ The :mod:`!pickle` module exports three classes, :class:`Pickler`,
433437
.. versionchanged:: 3.8
434438
The *buffers* argument was added.
435439

440+
.. audit-event:: pickle.Unpickler file pickle.Unpickler
441+
436442
.. method:: load()
437443

438444
Read the pickled representation of an object from the open file object

Lib/pickle.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1314,6 +1314,7 @@ def __init__(self, file, *, fix_imports=True,
13141314
default to 'ASCII' and 'strict', respectively. *encoding* can be
13151315
'bytes' to read these 8-bit string instances as bytes objects.
13161316
"""
1317+
sys.audit('pickle.Unpickler', file)
13171318
self._buffers = iter(buffers) if buffers is not None else None
13181319
self._file_readline = file.readline
13191320
self._file_read = file.read
@@ -1909,13 +1910,15 @@ def _dumps(obj, protocol=None, *, fix_imports=True, buffer_callback=None):
19091910

19101911
def _load(file, *, fix_imports=True, encoding="ASCII", errors="strict",
19111912
buffers=None):
1913+
sys.audit('pickle.load', file)
19121914
return _Unpickler(file, fix_imports=fix_imports, buffers=buffers,
19131915
encoding=encoding, errors=errors).load()
19141916

19151917
def _loads(s, /, *, fix_imports=True, encoding="ASCII", errors="strict",
19161918
buffers=None):
19171919
if isinstance(s, str):
19181920
raise TypeError("Can't load pickle from unicode string")
1921+
sys.audit('pickle.loads', s)
19191922
file = io.BytesIO(s)
19201923
return _Unpickler(file, fix_imports=fix_imports, buffers=buffers,
19211924
encoding=encoding, errors=errors).load()

Lib/test/audit-tests.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ def test_marshal():
137137

138138
def test_pickle():
139139
import pickle
140+
import io
140141

141142
class PicklePrint:
142143
def __reduce_ex__(self, p):
@@ -155,6 +156,34 @@ def __reduce_ex__(self, p):
155156
# pickles with no globals are okay
156157
pickle.loads(payload_2)
157158

159+
# Test pickle.loads audit event
160+
with TestHook() as hook:
161+
pickle.loads(payload_2)
162+
163+
actual = [a[0] for e, a in hook.seen if e == "pickle.loads"]
164+
assertSequenceEqual(actual, [payload_2])
165+
166+
# Test pickle.load audit event
167+
with TestHook() as hook:
168+
f = io.BytesIO(payload_2)
169+
pickle.load(f)
170+
171+
actual = [e for e, a in hook.seen if e == "pickle.load"]
172+
assertSequenceEqual(actual, ["pickle.load"])
173+
174+
# Test pickle.Unpickler audit event
175+
with TestHook() as hook:
176+
f = io.BytesIO(payload_2)
177+
pickle.Unpickler(f)
178+
179+
actual = [e for e, a in hook.seen if e == "pickle.Unpickler"]
180+
assertSequenceEqual(actual, ["pickle.Unpickler"])
181+
182+
# Test that pickle.loads can be blocked
183+
with TestHook(raise_on_events="pickle.loads") as hook:
184+
with assertRaises(RuntimeError):
185+
pickle.loads(payload_2)
186+
158187

159188
def test_monkeypatch():
160189
class A:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Added audit hooks for :func:`pickle.load`, :func:`pickle.loads`, and
2+
:class:`pickle.Unpickler` to allow monitoring and blocking of pickle
3+
deserialization. The new audit events are ``pickle.load``, ``pickle.loads``,
4+
and ``pickle.Unpickler``.

Modules/_pickle.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7514,6 +7514,9 @@ _pickle_Unpickler___init___impl(UnpicklerObject *self, PyObject *file,
75147514
const char *errors, PyObject *buffers)
75157515
/*[clinic end generated code: output=09f0192649ea3f85 input=ca4c1faea9553121]*/
75167516
{
7517+
if (PySys_Audit("pickle.Unpickler", "O", file) < 0) {
7518+
return -1;
7519+
}
75177520
BEGIN_USING_UNPICKLER(self, -1);
75187521
/* In case of multiple __init__() calls, clear previous content. */
75197522
if (self->read != NULL)
@@ -8043,6 +8046,9 @@ _pickle_load_impl(PyObject *module, PyObject *file, int fix_imports,
80438046
PyObject *buffers)
80448047
/*[clinic end generated code: output=250452d141c23e76 input=46c7c31c92f4f371]*/
80458048
{
8049+
if (PySys_Audit("pickle.load", "O", file) < 0) {
8050+
return NULL;
8051+
}
80468052
PyObject *result;
80478053
UnpicklerObject *unpickler = _Unpickler_New(module);
80488054

@@ -8104,6 +8110,9 @@ _pickle_loads_impl(PyObject *module, PyObject *data, int fix_imports,
81048110
PyObject *buffers)
81058111
/*[clinic end generated code: output=82ac1e6b588e6d02 input=b3615540d0535087]*/
81068112
{
8113+
if (PySys_Audit("pickle.loads", "O", data) < 0) {
8114+
return NULL;
8115+
}
81078116
PyObject *result;
81088117
UnpicklerObject *unpickler = _Unpickler_New(module);
81098118

0 commit comments

Comments
 (0)