deepset-ai/haystack · critical · DeserializationError
Refusing to deserialize '{handle}': it traverses into the in
Error message
Refusing to deserialize '{handle}': it traverses into the internal attribute '{name}', which can expose object internals (e.g. '__globals__', '__class__', '__builtins__') and is a known sandbox-escape gadget. If you trust the source of this data, load it with unsafe=True to bypass deserialization safety checks. What it means
Haystack refuses serialized handles that traverse into dunder or other internals-exposing attributes (e.g. __globals__, __class__, __builtins__), since attribute traversal on Python objects is a classic sandbox-escape gadget that can reach arbitrary code. This is a hardening check during deserialize_callable()/class imports; trusted data can bypass it with unsafe=True.
Source
Thrown at haystack/core/serialization_security.py:321
they never legitimately traverse into an object's internals. Dunder attributes (`__globals__`,
`__dict__`, `__class__`, `__builtins__`, `__subclasses__`, ...) and the frame/code accessors in
:data:`_UNSAFE_TRAVERSAL_ATTRS` are the classic sandbox-escape gadgets — e.g. `<func>.__globals__`
yields the defining module's live namespace, from which the allowlist state can be rewritten or
`__builtins__` (hence `eval`/`exec`) reached, regardless of any per-object identity check. The
module-granular allowlist does not stop this because the traversal stays inside an allowlisted
module. Bypassed in `unsafe=True` mode, which disables all deserialization safety checks by design.
:param name:
The attribute name about to be resolved from the current object in the walk.
:param handle:
The original serialized handle, used only for the error message.
:raises DeserializationError:
If `name` names an object-internals attribute.
"""
if _is_unsafe_deserialization():
return
if name.startswith("__") or name in _UNSAFE_TRAVERSAL_ATTRS:
raise DeserializationError(
f"Refusing to deserialize '{handle}': it traverses into the internal attribute '{name}', "
f"which can expose object internals (e.g. '__globals__', '__class__', '__builtins__') and is "
f"a known sandbox-escape gadget. If you trust the source of this data, load it with unsafe=True "
f"to bypass deserialization safety checks."
)
# Process-wide patterns set via allow_deserialization_module.
_extra_allowed_modules: list[str] = []
@mark_deserialization_internal
def allow_deserialization_module(pattern: str) -> None:
"""
Add a module pattern to the process-wide deserialization allowlist.
Once added, classes from modules matching the pattern can be deserialized from YAML / dict
representations until the process exits.View on GitHub (pinned to e318778c9b)
Solutions
- Remove the internal attribute reference from the serialized data and reference the public class/callable directly.
- If data is trusted and the reference is intentional, load with unsafe=True.
- Re-export the pipeline from the source environment so handles point at public APIs.
- Inspect the pipeline file for '__' paths before loading third-party pipelines.
Example fix
// before "callable": "my_module.MyComp.__class__.__init__" // after "callable": "my_module.MyComp"
Defensive patterns
Strategy: validation
Validate before calling
def contains_dunder_traversal(serialized: str) -> bool:
import re
return bool(re.search(r"__\w+__", serialized)) Type guard
def is_safe_handle(handle: str) -> bool:
return not any(part.startswith("__") for part in handle.split(".")) Try / catch
from haystack.core.errors import DeserializationError
try:
pipe = Pipeline.load(path)
except DeserializationError as e:
if "sandbox-escape" in str(e) or "internal attribute" in str(e):
raise # only bypass with unsafe=True if source is fully trusted
raise Prevention
- Scan serialized files for '__' paths before loading third-party pipelines
- Serialize public API names only; never store expression-like handles
- Keep unsafe=True usage audited and limited to trusted inputs
When it happens
Trigger: A serialized callable or class path containing '__'-prefixed segments or names in _UNSAFE_TRAVERSAL_ATTRS, resolved by deserialize_callable() or _import_class_by_name() from untrusted data.
Common situations: Maliciously crafted pipeline files; accidentally serialized expressions like 'obj.__class__.from_dict'; copying handles from non-Haystack serialized data into pipeline configs.
Related errors
- Refusing to deserialize an OutputAdapter with unsafe=True wh
- Refusing to deserialize an OutputAdapter with custom filters
- Refusing to deserialize a ConditionalRouter with unsafe=True
- Refusing to deserialize a ConditionalRouter with custom filt
- Refusing to deserialize '{handle}': it resolves to '{name}',
AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30).
Data as JSON: /api/errors/565af277c3d640aa.
Report an issue: GitHub.