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

  1. Remove the internal attribute reference from the serialized data and reference the public class/callable directly.
  2. If data is trusted and the reference is intentional, load with unsafe=True.
  3. Re-export the pipeline from the source environment so handles point at public APIs.
  4. 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

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


AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30). Data as JSON: /api/errors/565af277c3d640aa. Report an issue: GitHub.