Pickle deserialization attacks pose a critical threat to Python applications, especially in machine learning pipelines where model files are frequently shared and loaded. Pickleguard is a defense mechanism designed to detect and prevent malicious pickle payloads before they can execute harmful code. This article explores how Pickleguard works and how to integrate it into your Python applications.
The Problem: Pickle's Inherent Danger
As discussed in our previous article on pickle deserialization attacks, Python's pickle module can execute arbitrary code during deserialization. The core issue is the __reduce__ method and the REDUCE opcode, which allow attackers to call any Python function with arbitrary arguments.
Why Existing Solutions Fall Short
| Approach | Limitation |
|---|---|
| "Don't use pickle" | Impractical - pickle is embedded in PyTorch, scikit-learn, joblib |
| Restricted Unpickler | Requires manual allowlist maintenance, easy to misconfigure |
| Sandboxing | Performance overhead, complex setup, escape vulnerabilities |
| Code signing | Doesn't protect against compromised sources |
Pickleguard provides a practical middle ground: analyze pickle data before deserialization to detect malicious patterns.
How Pickleguard Works
Pickleguard operates on a defense-in-depth principle with multiple layers of protection:
┌─────────────────────────────────────────────────────────┐
│ Pickle Data Input │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Layer 1: Opcode Analysis │
│ Scan for dangerous opcodes (REDUCE, GLOBAL, etc.) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Layer 2: Import Inspection │
│ Check imported modules against blocklist/allowlist │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Layer 3: Call Graph Analysis │
│ Analyze function calls for suspicious patterns │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Layer 4: Safe Deserialization │
│ Load with restricted unpickler if all checks pass │
└─────────────────────────────────────────────────────────┘
Layer 1: Opcode Analysis
Pickleguard parses the pickle bytecode without executing it, identifying dangerous opcodes:
Dangerous Opcodes
| Opcode | Risk Level | Reason |
|---|---|---|
REDUCE (R) |
Critical | Calls arbitrary functions |
GLOBAL (c) |
High | Imports any module.name |
INST (i) |
Critical | Instantiates classes with args |
OBJ (o) |
Critical | Creates objects dynamically |
BUILD (b) |
Medium | Calls __setstate__ |
NEWOBJ |
High | Calls cls.__new__(cls, *args) |
NEWOBJ_EX |
High | Extended NEWOBJ with kwargs |
Analysis Example
from pickleguard import analyze_pickle
# Malicious payload
import pickle
import os
class Malicious:
def __reduce__(self):
return (os.system, ("whoami",))
payload = pickle.dumps(Malicious())
# Pickleguard analysis
result = analyze_pickle(payload)
print(result)
Output:
PickleAnalysis(
is_safe=False,
risk_level='CRITICAL',
findings=[
Finding(
type='DANGEROUS_IMPORT',
module='posix',
name='system',
description='System command execution function'
),
Finding(
type='REDUCE_CALL',
callable='posix.system',
args=['whoami'],
description='Arbitrary code execution via REDUCE'
)
]
)
Layer 2: Import Inspection
Pickleguard maintains categorized lists of Python modules and functions:
Blocklist Categories
BLOCKED_MODULES = {
# Command execution
'os', 'subprocess', 'commands', 'pty',
# Code execution
'builtins', 'code', 'codeop', 'compile',
# File system (dangerous operations)
'shutil', 'tempfile', 'fileinput',
# Network
'socket', 'urllib', 'http', 'ftplib',
# System access
'sys', 'ctypes', 'multiprocessing',
# Dangerous utilities
'pickle', 'marshal', 'shelve',
}
BLOCKED_FUNCTIONS = {
('builtins', 'eval'),
('builtins', 'exec'),
('builtins', 'compile'),
('builtins', '__import__'),
('builtins', 'open'),
('os', 'system'),
('os', 'popen'),
('os', 'spawn'),
('subprocess', 'call'),
('subprocess', 'run'),
('subprocess', 'Popen'),
}
ML-Safe Allowlist
For machine learning applications, Pickleguard provides a curated allowlist:
ML_SAFE_MODULES = {
# NumPy
('numpy', 'ndarray'),
('numpy', 'dtype'),
('numpy.core.multiarray', '_reconstruct'),
# PyTorch
('torch', 'Tensor'),
('torch._utils', '_rebuild_tensor_v2'),
('torch.nn.modules.*'), # Wildcard support
# Scikit-learn
('sklearn.*'),
('scipy.*'),
# Collections
('collections', 'OrderedDict'),
('collections', 'defaultdict'),
}
Layer 3: Call Graph Analysis
Pickleguard builds a call graph from the pickle opcodes to detect complex attack patterns:
Detecting Nested Attacks
# Attack using nested function calls
class NestedAttack:
def __reduce__(self):
# getattr(os, 'system')('whoami')
return (
getattr,
(__import__('os'), 'system')
)
Pickleguard traces through:
GLOBALimportsbuiltins.getattrREDUCEcalls__import__('os')REDUCEcallsgetattr(os_module, 'system')- Final result can execute commands
Detecting Obfuscated Payloads
# Obfuscated attack using string manipulation
class ObfuscatedAttack:
def __reduce__(self):
return (
eval,
("__" + "import" + "__('os').sys" + "tem('id')",)
)
Pickleguard's heuristics detect:
- String concatenation in arguments
- Common obfuscation patterns
- Encoded payloads (base64, hex)
Usage Examples
Basic Usage
from pickleguard import safe_load, PickleSecurityError
# Safe loading with automatic protection
try:
data = safe_load("model.pkl")
except PickleSecurityError as e:
print(f"Blocked malicious pickle: {e}")
Integration with PyTorch
from pickleguard.integrations import pytorch
# Patch torch.load globally
pytorch.patch()
# Now all torch.load calls are protected
import torch
model = torch.load("untrusted_model.pt") # Protected!
Custom Allowlist
from pickleguard import PickleGuard
guard = PickleGuard(
mode='allowlist',
allowed_modules={
('myapp.models', 'UserModel'),
('myapp.models', 'ProductModel'),
('numpy', '*'),
}
)
with open("data.pkl", "rb") as f:
data = guard.load(f)
CI/CD Integration
# scan_models.py - Run in CI pipeline
from pickleguard import scan_file
import sys
results = scan_file("models/production_model.pkl")
if not results.is_safe:
print("SECURITY ALERT: Malicious pickle detected!")
for finding in results.findings:
print(f" - {finding.type}: {finding.description}")
sys.exit(1)
print("Model passed security scan")
Performance Characteristics
Pickleguard is designed for minimal overhead:
| Operation | Overhead | Notes |
|---|---|---|
| Opcode scan | ~1-5ms | Linear in pickle size |
| Import check | ~0.1ms | Hash table lookup |
| Call graph | ~5-20ms | Complex payloads only |
| Total | <50ms | For typical ML models |
Benchmarks
Model Size | Native pickle.load | Pickleguard safe_load | Overhead
--------------|--------------------|----------------------|----------
1 MB | 45ms | 47ms | 4.4%
10 MB | 380ms | 392ms | 3.2%
100 MB | 3.2s | 3.25s | 1.6%
1 GB | 31s | 31.4s | 1.3%
The relative overhead decreases with larger files since the security scan is O(n) while deserialization dominates.
Defense Modes
Pickleguard supports multiple operational modes:
1. Strict Mode (Default)
Block any pickle with dangerous opcodes:
guard = PickleGuard(mode='strict')
# Blocks: REDUCE, GLOBAL, BUILD, INST, OBJ, NEWOBJ
# Only allows: basic types, lists, dicts, tuples
2. ML Mode
Allow common machine learning patterns:
guard = PickleGuard(mode='ml')
# Allows: numpy, torch, sklearn, scipy
# Blocks: os, subprocess, builtins.eval, etc.
3. Allowlist Mode
Explicit allowlist of permitted classes:
guard = PickleGuard(
mode='allowlist',
allowed_modules={...}
)
4. Audit Mode
Log but don't block (for monitoring):
guard = PickleGuard(mode='audit')
# Logs all findings to pickleguard.log
# Does not block loading
Handling Edge Cases
Legitimate Use of __reduce__
Some libraries legitimately use __reduce__ for serialization. Pickleguard handles this through:
- Curated allowlists for popular libraries
- Signature verification for known-safe patterns
- Custom exceptions for your own classes
# Register your own safe class
from pickleguard import register_safe_class
@register_safe_class
class MyCustomModel:
def __reduce__(self):
return (MyCustomModel, (self.weights,))
Nested/Recursive Structures
Pickleguard handles deeply nested structures without stack overflow:
# Iterative analysis, not recursive
def analyze_opcodes(data):
stack = []
for opcode, arg, pos in pickletools.genops(data):
# Iterative processing
...
Comparison with Alternatives
| Feature | Pickleguard | RestrictedUnpickler | Fickling | SafeTensors |
|---|---|---|---|---|
| Pre-execution scan | Yes | No | Yes | N/A |
| ML framework support | Yes | Manual | Limited | PyTorch only |
| Zero-config mode | Yes | No | No | Yes |
| Custom allowlists | Yes | Yes | No | No |
| Performance overhead | Low | None | Medium | None |
| Drop-in replacement | Yes | No | No | No |
| Detects obfuscation | Yes | No | Yes | N/A |
Deployment Recommendations
For Web Applications
# Flask/Django middleware
from pickleguard.middleware import PickleGuardMiddleware
app = Flask(__name__)
app.wsgi_app = PickleGuardMiddleware(app.wsgi_app)
For ML Pipelines
# Early initialization
import pickleguard
pickleguard.install() # Patches pickle, torch, joblib
# Rest of your code
import torch
model = torch.load("model.pt") # Now protected
For Data Processing
# Scan before processing
from pickleguard import scan_file
def process_uploaded_file(filepath):
result = scan_file(filepath)
if not result.is_safe:
raise SecurityError(f"Malicious file: {result.findings}")
with open(filepath, 'rb') as f:
return pickle.load(f)
Limitations
Pickleguard has some limitations to be aware of:
- Cannot prevent all attacks: Determined attackers may find bypasses
- Allowlist maintenance: ML libraries update frequently
- False positives: Some legitimate code may be blocked
- No runtime protection: Only scans before deserialization
Defense in Depth
Pickleguard should be one layer in a defense-in-depth strategy:
1. Avoid pickle when possible (use SafeTensors, JSON)
2. Sign and verify pickle files from trusted sources
3. Scan with Pickleguard before loading
4. Run untrusted code in sandboxed environments
5. Monitor for suspicious behavior at runtime
Getting Started
Installation
pip install pickleguard
Quick Start
from pickleguard import safe_load
# Replace pickle.load with safe_load
data = safe_load("data.pkl")
Global Protection
# Add to your application's startup
import pickleguard
pickleguard.install() # Protects all pickle operations
Conclusion
Pickle deserialization attacks remain a serious threat to Python applications, especially in the ML/AI ecosystem where model sharing is common. Pickleguard provides a practical defense layer that:
- Detects malicious payloads before execution
- Supports ML workflows with curated allowlists
- Minimal overhead for production use
- Easy integration with existing code
While no single tool can provide complete protection, Pickleguard significantly raises the bar for attackers and provides visibility into potential threats.
Resources: