Summary
pyod.utils.persistence.load() performs joblib.load(path) before checking whether the loaded object is a PyOD persistence envelope. Because joblib/pickle deserialization can execute Python reducers during loading, a crafted artifact can execute code before _is_envelope(), schema validation, dependency-version checks, or strict=True rejection logic run.
This is not a default network-exposed issue in PyOD itself. The impact applies when an application loads model artifacts that are attacker-controlled or not fully trusted, such as user-uploaded models, third-party artifacts, shared model directories, CI artifacts, or external model registries.
Affected component
pyod/utils/persistence.py
- Function:
load(path, strict=False, return_metadata=False)
- Observed version in this checkout:
3.5.2
Relevant code path
The current order is:
load() calls joblib.load(path).
- The returned object is passed to
_handle_loaded_object().
_handle_loaded_object() then checks _is_envelope(obj).
- Schema/version/strict-mode checks occur only after the object has already been unpickled.
In other words, the envelope shape is not a pre-deserialization security boundary.
Reproduction overview
A local proof of concept used a top-level dict with the expected envelope keys:
_pyod_persistence_version
model
The model value was an object whose pickle reducer wrote a harmless marker file during deserialization. Loading the artifact with load(path, strict=True) produced this result:
pre_dump_is_envelope: True
marker_after_dump: False
load_result: 0
marker_after_load: True
marker_content: executed-before-validation
This confirms that reducer execution happens during joblib.load() and before strict-mode or envelope validation can reject anything.
Expected behavior
If the envelope is intended to provide a validation boundary, metadata should be inspectable before deserializing the model payload, or the API should make the trusted-only boundary explicit for all callers.
Actual behavior
load() unpickles the entire artifact first. A malicious artifact can execute code with the privileges of the Python process before _is_envelope() and related validation logic run.
Impact
If an attacker can cause a victim application to call pyod.utils.persistence.load() on an attacker-controlled artifact, this can lead to arbitrary code execution under the loading process account. Consequences may include credential theft, data exfiltration, model/result tampering, persistence, lateral movement, or denial of service, depending on the deployment environment.
The severity is contextual: it is high for ML platforms, automated evaluation systems, CI/CD pipelines, or services that import external model artifacts; it is lower when users only load artifacts produced by their own trusted training pipeline.
Notes
The documentation already warns that pickle/joblib can execute arbitrary Python code and that the persistence wrapper does not sandbox unpickling. This report is specifically about the validation order in load(): strict=True and envelope validation do not mitigate malicious pickle execution because they run after deserialization.
Summary
pyod.utils.persistence.load()performsjoblib.load(path)before checking whether the loaded object is a PyOD persistence envelope. Because joblib/pickle deserialization can execute Python reducers during loading, a crafted artifact can execute code before_is_envelope(), schema validation, dependency-version checks, orstrict=Truerejection logic run.This is not a default network-exposed issue in PyOD itself. The impact applies when an application loads model artifacts that are attacker-controlled or not fully trusted, such as user-uploaded models, third-party artifacts, shared model directories, CI artifacts, or external model registries.
Affected component
pyod/utils/persistence.pyload(path, strict=False, return_metadata=False)3.5.2Relevant code path
The current order is:
load()callsjoblib.load(path)._handle_loaded_object()._handle_loaded_object()then checks_is_envelope(obj).In other words, the envelope shape is not a pre-deserialization security boundary.
Reproduction overview
A local proof of concept used a top-level dict with the expected envelope keys:
_pyod_persistence_versionmodelThe
modelvalue was an object whose pickle reducer wrote a harmless marker file during deserialization. Loading the artifact withload(path, strict=True)produced this result:This confirms that reducer execution happens during
joblib.load()and before strict-mode or envelope validation can reject anything.Expected behavior
If the envelope is intended to provide a validation boundary, metadata should be inspectable before deserializing the model payload, or the API should make the trusted-only boundary explicit for all callers.
Actual behavior
load()unpickles the entire artifact first. A malicious artifact can execute code with the privileges of the Python process before_is_envelope()and related validation logic run.Impact
If an attacker can cause a victim application to call
pyod.utils.persistence.load()on an attacker-controlled artifact, this can lead to arbitrary code execution under the loading process account. Consequences may include credential theft, data exfiltration, model/result tampering, persistence, lateral movement, or denial of service, depending on the deployment environment.The severity is contextual: it is high for ML platforms, automated evaluation systems, CI/CD pipelines, or services that import external model artifacts; it is lower when users only load artifacts produced by their own trusted training pipeline.
Notes
The documentation already warns that pickle/joblib can execute arbitrary Python code and that the persistence wrapper does not sandbox unpickling. This report is specifically about the validation order in
load():strict=Trueand envelope validation do not mitigate malicious pickle execution because they run after deserialization.