Skip to content

Commit 8cadb61

Browse files
committed
Add audit hooks for pickle.load, pickle.loads, and pickle.Unpickler
Add sys.audit / PySys_Audit calls for deserialization entry points in the pickle module to allow security monitoring of pickle usage. This complements the existing pickle.find_class audit event by providing visibility into when pickle deserialization is initiated, not just when classes are resolved during unpickling. New audit events: - pickle.load(file): raised when pickle.load() is called - pickle.loads(data): raised when pickle.loads() is called - pickle.Unpickler(file): raised when an Unpickler is instantiated These hooks are implemented in both the Python (Lib/pickle.py) and C (Modules/_pickle.c) implementations. https://claude.ai/code/session_01HKopnL4QijaMQU4drGEefL
1 parent e1dbe22 commit 8cadb61

4 files changed

Lines changed: 48 additions & 0 deletions

File tree

Doc/library/pickle.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,14 @@ process more convenient:
236236
Arguments *file*, *fix_imports*, *encoding*, *errors*, *strict* and *buffers*
237237
have the same meaning as in the :class:`Unpickler` constructor.
238238

239+
.. audit-event:: pickle.load file pickle.load
240+
239241
.. versionchanged:: 3.8
240242
The *buffers* argument was added.
241243

244+
.. versionchanged:: next
245+
Added the ``pickle.load`` audit event.
246+
242247
.. function:: loads(data, /, *, fix_imports=True, encoding="ASCII", errors="strict", buffers=None)
243248

244249
Return the reconstituted object hierarchy of the pickled representation
@@ -251,9 +256,14 @@ process more convenient:
251256
Arguments *fix_imports*, *encoding*, *errors*, *strict* and *buffers*
252257
have the same meaning as in the :class:`Unpickler` constructor.
253258

259+
.. audit-event:: pickle.loads data pickle.loads
260+
254261
.. versionchanged:: 3.8
255262
The *buffers* argument was added.
256263

264+
.. versionchanged:: next
265+
Added the ``pickle.loads`` audit event.
266+
257267

258268
The :mod:`pickle` module defines three exceptions:
259269

@@ -417,9 +427,14 @@ The :mod:`pickle` module exports three classes, :class:`Pickler`,
417427
an :ref:`out-of-band <pickle-oob>` buffer view. Such buffers have been
418428
given in order to the *buffer_callback* of a Pickler object.
419429

430+
.. audit-event:: pickle.Unpickler file pickle.Unpickler
431+
420432
.. versionchanged:: 3.8
421433
The *buffers* argument was added.
422434

435+
.. versionchanged:: next
436+
Added the ``pickle.Unpickler`` audit event.
437+
423438
.. method:: load()
424439

425440
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
@@ -1218,6 +1218,7 @@ def __init__(self, file, *, fix_imports=True,
12181218
default to 'ASCII' and 'strict', respectively. *encoding* can be
12191219
'bytes' to read these 8-bit string instances as bytes objects.
12201220
"""
1221+
sys.audit('pickle.Unpickler', file)
12211222
self._buffers = iter(buffers) if buffers is not None else None
12221223
self._file_readline = file.readline
12231224
self._file_read = file.read
@@ -1803,13 +1804,15 @@ def _dumps(obj, protocol=None, *, fix_imports=True, buffer_callback=None):
18031804

18041805
def _load(file, *, fix_imports=True, encoding="ASCII", errors="strict",
18051806
buffers=None):
1807+
sys.audit('pickle.load', file)
18061808
return _Unpickler(file, fix_imports=fix_imports, buffers=buffers,
18071809
encoding=encoding, errors=errors).load()
18081810

18091811
def _loads(s, /, *, fix_imports=True, encoding="ASCII", errors="strict",
18101812
buffers=None):
18111813
if isinstance(s, str):
18121814
raise TypeError("Can't load pickle from unicode string")
1815+
sys.audit('pickle.loads', s)
18131816
file = io.BytesIO(s)
18141817
return _Unpickler(file, fix_imports=fix_imports, buffers=buffers,
18151818
encoding=encoding, errors=errors).load()

Lib/test/audit-tests.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ def test_marshal():
135135

136136
def test_pickle():
137137
import pickle
138+
import io
138139

139140
class PicklePrint:
140141
def __reduce_ex__(self, p):
@@ -153,6 +154,21 @@ def __reduce_ex__(self, p):
153154
# pickles with no globals are okay
154155
pickle.loads(payload_2)
155156

157+
# Test that pickle.loads raises the pickle.loads audit event
158+
with TestHook(raise_on_events="pickle.loads") as hook:
159+
with assertRaises(RuntimeError):
160+
pickle.loads(payload_2)
161+
162+
# Test that pickle.load raises the pickle.load audit event
163+
with TestHook(raise_on_events="pickle.load") as hook:
164+
with assertRaises(RuntimeError):
165+
pickle.load(io.BytesIO(payload_2))
166+
167+
# Test that pickle.Unpickler raises the pickle.Unpickler audit event
168+
with TestHook(raise_on_events="pickle.Unpickler") as hook:
169+
with assertRaises(RuntimeError):
170+
pickle.Unpickler(io.BytesIO(payload_2))
171+
156172

157173
def test_monkeypatch():
158174
class A:

Modules/_pickle.c

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7287,6 +7287,10 @@ _pickle_Unpickler___init___impl(UnpicklerObject *self, PyObject *file,
72877287
const char *errors, PyObject *buffers)
72887288
/*[clinic end generated code: output=09f0192649ea3f85 input=ca4c1faea9553121]*/
72897289
{
7290+
if (PySys_Audit("pickle.Unpickler", "O", file) < 0) {
7291+
return -1;
7292+
}
7293+
72907294
BEGIN_USING_UNPICKLER(self, -1);
72917295
/* In case of multiple __init__() calls, clear previous content. */
72927296
if (self->read != NULL)
@@ -7811,6 +7815,11 @@ _pickle_load_impl(PyObject *module, PyObject *file, int fix_imports,
78117815
/*[clinic end generated code: output=250452d141c23e76 input=46c7c31c92f4f371]*/
78127816
{
78137817
PyObject *result;
7818+
7819+
if (PySys_Audit("pickle.load", "O", file) < 0) {
7820+
return NULL;
7821+
}
7822+
78147823
UnpicklerObject *unpickler = _Unpickler_New(module);
78157824

78167825
if (unpickler == NULL)
@@ -7872,6 +7881,11 @@ _pickle_loads_impl(PyObject *module, PyObject *data, int fix_imports,
78727881
/*[clinic end generated code: output=82ac1e6b588e6d02 input=b3615540d0535087]*/
78737882
{
78747883
PyObject *result;
7884+
7885+
if (PySys_Audit("pickle.loads", "O", data) < 0) {
7886+
return NULL;
7887+
}
7888+
78757889
UnpicklerObject *unpickler = _Unpickler_New(module);
78767890

78777891
if (unpickler == NULL)

0 commit comments

Comments
 (0)