1+ import pytest
2+ import pickle
3+ import os
4+ import sys
5+ import subprocess
6+ import tempfile
7+ import struct
8+
9+
10+ # Helper to create malicious pickle payloads that attempt code execution
11+ def make_exec_payload (command ):
12+ """Create a pickle payload that attempts to execute a system command."""
13+ import io
14+
15+ class MaliciousReduce :
16+ def __reduce__ (self ):
17+ return (os .system , (command ,))
18+
19+ return pickle .dumps (MaliciousReduce ())
20+
21+
22+ def make_import_payload (module , attr ):
23+ """Create a pickle payload using GLOBAL opcode to import arbitrary modules."""
24+ # Manually craft a pickle that calls getattr on a module
25+ import io
26+ buf = io .BytesIO ()
27+ buf .write (b'\x80 \x04 ' ) # PROTO 4
28+ buf .write (b'c' ) # GLOBAL opcode
29+ buf .write (f'{ module } \n { attr } \n ' .encode ())
30+ buf .write (b')' ) # EMPTY_TUPLE
31+ buf .write (b'R' ) # REDUCE
32+ buf .write (b'.' ) # STOP
33+ return buf .getvalue ()
34+
35+
36+ def make_reduce_subprocess_payload (cmd ):
37+ """Create payload using subprocess to execute commands."""
38+ import io
39+
40+ class SubprocessPayload :
41+ def __reduce__ (self ):
42+ import subprocess
43+ return (subprocess .check_output , ([cmd ],))
44+
45+ return pickle .dumps (SubprocessPayload ())
46+
47+
48+ # Sentinel file to detect if code execution occurred
49+ SENTINEL_FILE = tempfile .mktemp (suffix = '_pickle_security_test_sentinel' )
50+
51+
52+ @pytest .fixture (autouse = True )
53+ def cleanup_sentinel ():
54+ """Ensure sentinel file is cleaned up before and after each test."""
55+ if os .path .exists (SENTINEL_FILE ):
56+ os .remove (SENTINEL_FILE )
57+ yield
58+ if os .path .exists (SENTINEL_FILE ):
59+ os .remove (SENTINEL_FILE )
60+
61+
62+ def make_sentinel_payload ():
63+ """Create a pickle payload that tries to create a sentinel file if executed."""
64+ class SentinelPayload :
65+ def __reduce__ (self ):
66+ return (open , (SENTINEL_FILE , 'w' ))
67+ return pickle .dumps (SentinelPayload ())
68+
69+
70+ def make_eval_payload (code_str ):
71+ """Create a pickle payload that tries to eval arbitrary code."""
72+ class EvalPayload :
73+ def __reduce__ (self ):
74+ return (eval , (code_str ,))
75+ return pickle .dumps (EvalPayload ())
76+
77+
78+ def make_exec_file_payload (filepath ):
79+ """Create a pickle payload that tries to read a sensitive file."""
80+ class ReadFilePayload :
81+ def __reduce__ (self ):
82+ return (open , (filepath , 'r' ))
83+ return pickle .dumps (ReadFilePayload ())
84+
85+
86+ @pytest .mark .parametrize ("payload_name,payload_factory" , [
87+ ("os_system_touch_sentinel" , lambda : make_exec_payload (f'touch { SENTINEL_FILE } ' )),
88+ ("sentinel_file_creation" , make_sentinel_payload ),
89+ ("eval_dangerous_code" , lambda : make_eval_payload ("__import__('os').getcwd()" )),
90+ ("read_passwd" , lambda : make_exec_file_payload ('/etc/passwd' )),
91+ ("subprocess_ls" , lambda : make_reduce_subprocess_payload ('ls' )),
92+ ])
93+ def test_pickle_cli_does_not_execute_arbitrary_code (payload_name , payload_factory , tmp_path ):
94+ """Invariant: The pickle CLI interface must not allow arbitrary code execution
95+ through malicious pickle payloads. When processing adversarial pickle data,
96+ the system should either reject the input, sandbox execution, or otherwise
97+ prevent malicious __reduce__ methods from executing arbitrary system commands."""
98+
99+ payload = payload_factory ()
100+
101+ # Write payload to a temp file
102+ payload_file = tmp_path / f"malicious_{ payload_name } .pkl"
103+ payload_file .write_bytes (payload )
104+
105+ # Attempt to run the pickle CLI with the malicious payload
106+ result = subprocess .run (
107+ [sys .executable , '-m' , 'pickle' , str (payload_file )],
108+ capture_output = True ,
109+ text = True ,
110+ timeout = 10 ,
111+ cwd = str (tmp_path )
112+ )
113+
114+ # The sentinel file must NOT have been created by the payload
115+ # If it was created, arbitrary code execution occurred
116+ assert not os .path .exists (SENTINEL_FILE ), (
117+ f"SECURITY VIOLATION: Pickle CLI executed arbitrary code from payload '{ payload_name } '. "
118+ f"Sentinel file was created, indicating __reduce__ method was invoked with dangerous operations. "
119+ f"stdout: { result .stdout } , stderr: { result .stderr } "
120+ )
121+
122+
123+ @pytest .mark .parametrize ("payload_bytes" , [
124+ # Raw crafted pickle opcodes attempting to call os.system
125+ b'\x80 \x04 \x95 \x1f \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x8c \x02 os\x94 \x8c \x06 system\x94 \x93 \x8c \x02 ls\x94 \x85 \x94 R\x94 .' ,
126+ # Pickle with REDUCE opcode on builtins.eval
127+ b'\x80 \x04 \x95 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x00 \x8c \x08 builtins\x94 \x8c \x04 eval\x94 \x93 \x8c \x04 pass\x94 \x85 \x94 R\x94 .' ,
128+ # Empty/malformed pickle
129+ b'' ,
130+ # Truncated pickle
131+ b'\x80 \x04 ' ,
132+ # Pickle with invalid opcodes
133+ b'\xff \xff \xff \xff ' ,
134+ # Very large pickle header
135+ b'\x80 \x04 ' + b'\x00 ' * 1000 ,
136+ ])
137+ def test_pickle_cli_handles_adversarial_bytes_safely (payload_bytes , tmp_path ):
138+ """Invariant: The pickle CLI must handle adversarial byte sequences without
139+ causing uncontrolled code execution or system compromise. The process should
140+ either complete safely or fail with an error, but must not execute injected code."""
141+
142+ payload_file = tmp_path / "adversarial.pkl"
143+ payload_file .write_bytes (payload_bytes )
144+
145+ result = subprocess .run (
146+ [sys .executable , '-m' , 'pickle' , str (payload_file )],
147+ capture_output = True ,
148+ text = True ,
149+ timeout = 10 ,
150+ cwd = str (tmp_path )
151+ )
152+
153+ # The process must not have created the sentinel file
154+ assert not os .path .exists (SENTINEL_FILE ), (
155+ f"SECURITY VIOLATION: Adversarial pickle bytes caused code execution. "
156+ f"Payload (hex): { payload_bytes .hex ()} "
157+ )
158+
159+ # The process must terminate (not hang indefinitely) - already enforced by timeout
160+
161+
162+ def test_pickle_cli_stdin_does_not_execute_code ():
163+ """Invariant: When the pickle CLI reads from stdin, it must not execute
164+ arbitrary code embedded in the pickle stream via __reduce__ methods."""
165+
166+ # Create a payload that would create a sentinel file if executed
167+ class SentinelPayload :
168+ def __reduce__ (self ):
169+ return (open , (SENTINEL_FILE , 'w' ))
170+
171+ payload = pickle .dumps (SentinelPayload ())
172+
173+ result = subprocess .run (
174+ [sys .executable , '-m' , 'pickle' , '-' ],
175+ input = payload ,
176+ capture_output = True ,
177+ timeout = 10 ,
178+ )
179+
180+ # Sentinel file must not exist - no code execution should have occurred
181+ assert not os .path .exists (SENTINEL_FILE ), (
182+ "SECURITY VIOLATION: Pickle CLI executed arbitrary code from stdin payload. "
183+ "The __reduce__ method was invoked, creating the sentinel file. "
184+ f"Return code: { result .returncode } , stderr: { result .stderr .decode ()} "
185+ )
186+
187+
188+ def test_pickle_deserialization_invariant_no_os_commands ():
189+ """Invariant: Deserializing pickle data through the CLI must never result
190+ in OS command execution. This is a fundamental security boundary that
191+ must be maintained regardless of the pickle content."""
192+
193+ dangerous_commands = [
194+ f'touch { SENTINEL_FILE } ' ,
195+ f'echo pwned > { SENTINEL_FILE } ' ,
196+ f'python3 -c "open(\' { SENTINEL_FILE } \' , \' w\' ).close()"' ,
197+ ]
198+
199+ for cmd in dangerous_commands :
200+ if os .path .exists (SENTINEL_FILE ):
201+ os .remove (SENTINEL_FILE )
202+
203+ payload = pickle .dumps (type ('Exploit' , (), {
204+ '__reduce__' : lambda self , c = cmd : (os .system , (c ,))
205+ })())
206+
207+ with tempfile .NamedTemporaryFile (suffix = '.pkl' , delete = False ) as f :
208+ f .write (payload )
209+ fname = f .name
210+
211+ try :
212+ result = subprocess .run (
213+ [sys .executable , '-m' , 'pickle' , fname ],
214+ capture_output = True ,
215+ timeout = 10 ,
216+ )
217+ finally :
218+ os .unlink (fname )
219+
220+ assert not os .path .exists (SENTINEL_FILE ), (
221+ f"SECURITY VIOLATION: OS command '{ cmd } ' was executed via pickle CLI. "
222+ "The pickle module CLI must not allow arbitrary OS command execution."
223+ )
0 commit comments