Subclass pickle.Unpickler and override find_class so it returns only names on an allow list and raises an error for everything else. Python's documentation describes this as the way to forbid globals or restrict them to a safe subset when you cannot avoid unpickling.
Why find_class is the control point
By default, unpickling imports any class or function named in the data. The Python docs call this unacceptable for many applications, because it lets the unpickler import and invoke arbitrary code. They explain that find_class is called whenever a global is requested, so you can block it or limit it.
The docs include an example that allows a handful of built-in types and rejects the rest, raising an error that names the forbidden global.
import builtins
import io
import pickle
SAFE = {("builtins", "range"), ("builtins", "set"), ("builtins", "frozenset")}
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if (module, name) in SAFE:
return getattr(__import__(module, fromlist=[name]), name)
raise pickle.UnpicklingError(f"global {module}.{name} is forbidden")
def restricted_loads(data: bytes):
return RestrictedUnpickler(io.BytesIO(data)).load()
Building your allow list
- Inspect first. Disassemble the pickle with pickletools without loading it, and list the imports it references.
- Decide per name. Allow specific classes, not whole modules.
- Start empty. Add names only when a real file needs them.
- Log denials. A rejected name is either a bug or an attack.
- Review the list like code, with an owner.
import pickletools
with open("data.pkl", "rb") as f:
for op, arg, pos in pickletools.genops(f):
if op.name in ("GLOBAL", "STACK_GLOBAL"):
print(pos, op.name, arg)
Note that STACK_GLOBAL takes its names from the stack, so the argument may print as None. Use the full disassembly when reading those.
Limits to keep in mind
| Limit | Why it matters |
|---|---|
| An allowed class can still have unsafe behavior | Only allow types whose constructors you understand |
| Hugging Face points to a sophisticated exploit against a custom unpickler | Restriction is hard to get right, so keep lists tiny |
| It protects one loader, not the whole pipeline | Other code paths may still call plain load |
| You must route every load through it | Search the codebase for direct calls |
Hugging Face's pickle documentation says its own authors looked at custom unpicklers and linked to an exploit showing that a naive one may not hold up, and suggested reading the opcodes directly as an alternative. That is a reason to treat find_class as a layer, not a guarantee.
Better options when you can choose
Python's docs suggest json or other safer formats for untrusted data. For model weights, safetensors avoids the problem altogether. For integrity, the docs mention signing data with hmac so you can tell if it changed.
Where Safeguard fits
Safeguard covers the dependency side of the stack with SBOM and dependency scanning, reachability analysis and gates on pull requests. A free scan is at gold.safeguard.sh/scan. See also what a pickle file is.
Quick check
Is a restricted unpickler safe for untrusted files?
It reduces risk, but it is not a guarantee. Keep the allow list minimal, and prefer a format that cannot run code.
Where does find_class get called?
Whenever the data requests a global such as a class or function, which is why overriding it gives you control.
Sources
Last reviewed: 19 October 2026.