Skip to content

Commit 9fa86b3

Browse files
committed
gh-XXXXX: Add audit hooks for pickle.load, pickle.loads, and pickle.Unpickler
Add sys.audit() calls to raise audit events when pickle deserializes data, enabling security monitoring and access control via Python's audit hook system. New audit events: - pickle.load(file): raised by pickle.load() - pickle.loads(data): raised by pickle.loads() - pickle.Unpickler(file): raised when pickle.Unpickler() is instantiated Both the C extension (_pickle) and the pure Python fallback (pickle) emit the same events, ensuring consistent behavior regardless of which implementation is active. https://claude.ai/code/session_01BWEWpGi5yZzcLHwjh9FWtP
1 parent 149c465 commit 9fa86b3

4 files changed

Lines changed: 38 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 file 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", 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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ def test_marshal():
136136

137137

138138
def test_pickle():
139+
import io
139140
import pickle
140141

141142
class PicklePrint:
@@ -155,6 +156,24 @@ 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+
actual = [a[0] for e, a in hook.seen if e == "pickle.loads"]
163+
assertSequenceEqual(actual, [payload_2])
164+
165+
# Test pickle.load audit event
166+
with TestHook() as hook:
167+
pickle.load(io.BytesIO(payload_2))
168+
actual = [e for e, a in hook.seen if e == "pickle.load"]
169+
assertSequenceEqual(actual, ["pickle.load"])
170+
171+
# Test pickle.Unpickler audit event
172+
with TestHook() as hook:
173+
pickle.Unpickler(io.BytesIO(payload_2))
174+
actual = [e for e, a in hook.seen if e == "pickle.Unpickler"]
175+
assertSequenceEqual(actual, ["pickle.Unpickler"])
176+
158177

159178
def test_monkeypatch():
160179
class A:

Modules/_pickle.c

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7515,6 +7515,10 @@ _pickle_Unpickler___init___impl(UnpicklerObject *self, PyObject *file,
75157515
/*[clinic end generated code: output=09f0192649ea3f85 input=ca4c1faea9553121]*/
75167516
{
75177517
BEGIN_USING_UNPICKLER(self, -1);
7518+
if (PySys_Audit("pickle.Unpickler", "O", file) < 0) {
7519+
END_USING_UNPICKLER(self);
7520+
return -1;
7521+
}
75187522
/* In case of multiple __init__() calls, clear previous content. */
75197523
if (self->read != NULL)
75207524
(void)Unpickler_clear((PyObject *)self);
@@ -8043,6 +8047,9 @@ _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", "O", file) < 0) {
8051+
return NULL;
8052+
}
80468053
PyObject *result;
80478054
UnpicklerObject *unpickler = _Unpickler_New(module);
80488055

@@ -8104,6 +8111,9 @@ _pickle_loads_impl(PyObject *module, PyObject *data, int fix_imports,
81048111
PyObject *buffers)
81058112
/*[clinic end generated code: output=82ac1e6b588e6d02 input=b3615540d0535087]*/
81068113
{
8114+
if (PySys_Audit("pickle.loads", "O", data) < 0) {
8115+
return NULL;
8116+
}
81078117
PyObject *result;
81088118
UnpicklerObject *unpickler = _Unpickler_New(module);
81098119

0 commit comments

Comments
 (0)