Skip to content

Commit 90cc8dd

Browse files
committed
Add pickle.load, pickle.loads, and pickle.Unpickler audit events
Add audit hooks that fire when unpickling operations are initiated, complementing the existing pickle.find_class audit event. This allows audit hooks to detect and block untrusted deserialization at the entry point, before any pickle opcodes are processed. New audit events: - pickle.load: raised by pickle.load() with no arguments - pickle.loads: raised by pickle.loads() with the data argument - pickle.Unpickler: raised by Unpickler.__init__() with the file argument Both the C (_pickle) and Python (pickle) implementations are covered. https://claude.ai/code/session_014cvXKnYLMAjdJpE59sKGUi
1 parent 149c465 commit 90cc8dd

5 files changed

Lines changed: 58 additions & 0 deletions

File tree

Doc/library/pickle.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ process more convenient:
240240
Arguments *file*, *fix_imports*, *encoding*, *errors*, *strict* and *buffers*
241241
have the same meaning as in the :class:`Unpickler` constructor.
242242

243+
.. audit-event:: pickle.load "" pickle.load
244+
243245
.. versionchanged:: 3.8
244246
The *buffers* argument was added.
245247

@@ -255,6 +257,8 @@ process more convenient:
255257
Arguments *fix_imports*, *encoding*, *errors*, *strict* and *buffers*
256258
have the same meaning as in the :class:`Unpickler` constructor.
257259

260+
.. audit-event:: pickle.loads data pickle.loads
261+
258262
.. versionchanged:: 3.8
259263
The *buffers* argument was added.
260264

@@ -430,6 +434,8 @@ The :mod:`!pickle` module exports three classes, :class:`Pickler`,
430434
an :ref:`out-of-band <pickle-oob>` buffer view. Such buffers have been
431435
given in order to the *buffer_callback* of a Pickler object.
432436

437+
.. audit-event:: pickle.Unpickler file pickle.Unpickler
438+
433439
.. versionchanged:: 3.8
434440
The *buffers* argument was added.
435441

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')
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: 33 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,38 @@ def __reduce_ex__(self, p):
155156
# pickles with no globals are okay
156157
pickle.loads(payload_2)
157158

159+
# Test that pickle.loads audit event is raised with the data argument
160+
with TestHook() as hook:
161+
pickle.loads(payload_2)
162+
loads_events = [(e, a) for e, a in hook.seen if e == "pickle.loads"]
163+
assertEqual(len(loads_events), 1)
164+
assertEqual(loads_events[0][1][0], payload_2)
165+
166+
# Test that pickle.load audit event is raised
167+
with TestHook() as hook:
168+
f = io.BytesIO(payload_2)
169+
pickle.load(f)
170+
load_events = [e for e, a in hook.seen if e == "pickle.load"]
171+
assertEqual(len(load_events), 1)
172+
173+
# Test that pickle.Unpickler audit event is raised
174+
with TestHook() as hook:
175+
f = io.BytesIO(payload_2)
176+
pickle.Unpickler(f).load()
177+
unpickler_events = [(e, a) for e, a in hook.seen if e == "pickle.Unpickler"]
178+
assertEqual(len(unpickler_events), 1)
179+
180+
# Test that pickle.loads can be blocked by an audit hook
181+
with TestHook(raise_on_events="pickle.loads") as hook:
182+
with assertRaises(RuntimeError):
183+
pickle.loads(payload_2)
184+
185+
# Test that pickle.load can be blocked by an audit hook
186+
with TestHook(raise_on_events="pickle.load") as hook:
187+
with assertRaises(RuntimeError):
188+
f = io.BytesIO(payload_2)
189+
pickle.load(f)
190+
158191

159192
def test_monkeypatch():
160193
class A:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Added audit events ``pickle.load``, ``pickle.loads``, and
2+
``pickle.Unpickler`` that are raised when unpickling operations are initiated.
3+
These complement the existing ``pickle.find_class`` audit event and allow audit
4+
hooks to detect and prevent untrusted deserialization.

Modules/_pickle.c

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7514,6 +7514,10 @@ _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+
}
7520+
75177521
BEGIN_USING_UNPICKLER(self, -1);
75187522
/* In case of multiple __init__() calls, clear previous content. */
75197523
if (self->read != NULL)
@@ -8043,6 +8047,10 @@ _pickle_load_impl(PyObject *module, PyObject *file, int fix_imports,
80438047
PyObject *buffers)
80448048
/*[clinic end generated code: output=250452d141c23e76 input=46c7c31c92f4f371]*/
80458049
{
8050+
if (PySys_Audit("pickle.load", NULL) < 0) {
8051+
return NULL;
8052+
}
8053+
80468054
PyObject *result;
80478055
UnpicklerObject *unpickler = _Unpickler_New(module);
80488056

@@ -8104,6 +8112,10 @@ _pickle_loads_impl(PyObject *module, PyObject *data, int fix_imports,
81048112
PyObject *buffers)
81058113
/*[clinic end generated code: output=82ac1e6b588e6d02 input=b3615540d0535087]*/
81068114
{
8115+
if (PySys_Audit("pickle.loads", "O", data) < 0) {
8116+
return NULL;
8117+
}
8118+
81078119
PyObject *result;
81088120
UnpicklerObject *unpickler = _Unpickler_New(module);
81098121

0 commit comments

Comments
 (0)