deepset-ai/haystack · critical · DeserializationError

Refusing to deserialize '{handle}': it resolves to the built

Error message

Refusing to deserialize '{handle}': it resolves to the builtin '{name}', which is blocked because it can be used to execute code, import modules, access the filesystem, or escape via attribute access. If you trust the source of this data, load it with unsafe=True to bypass deserialization safety checks.

What it means

Certain Python builtins (e.g. eval, exec, open, __import__, getattr) are denied during deserialization because resolving serialized handles to them enables code execution, filesystem access, or attribute-based escape. deserialize_callable() blocks them for untrusted data; trusted data can bypass with unsafe=True.

Source

Thrown at haystack/core/serialization_security.py:490

    Reject `resolved` if it is a builtin callable that is unsafe to resolve from serialized data.

    Used by the callable-resolution path (`deserialize_callable`). Raises
    :class:`DeserializationError` for the primitives in :data:`_DENIED_BUILTIN_NAMES`, which can
    execute code, import modules, touch the filesystem, or escape via attribute/namespace access.
    The block applies even though `builtins` is on the allowlist, because the allowlist is
    module-granular. It is intentionally bypassed in `unsafe=True` mode, which disables all
    deserialization safety checks by design.

    :param resolved:
        The object resolved from the serialized handle.
    :param handle:
        The original serialized handle, used only for the error message.
    """
    if _is_unsafe_deserialization():
        return
    if _is_denied_builtin(resolved):
        name = getattr(resolved, "__name__", str(resolved))
        raise DeserializationError(
            f"Refusing to deserialize '{handle}': it resolves to the builtin '{name}', which is "
            f"blocked because it can be used to execute code, import modules, access the "
            f"filesystem, or escape via attribute access. If you trust the source of this data, "
            f"load it with unsafe=True to bypass deserialization safety checks."
        )


def _check_not_denied_callable(resolved: object, handle: str) -> None:
    """
    Reject `resolved` if it is an import primitive that is unsafe to resolve from serialized data.

    Used by the callable-resolution path (`deserialize_callable`) as a companion to
    :func:`_check_not_denied_builtin`. It blocks the non-builtin import primitives in
    :data:`_DENIED_CALLABLE_OBJECTS` / :data:`_DENIED_CALLABLE_QUALNAMES` (e.g.
    `importlib.import_module`, `haystack.utils.type_serialization.thread_safe_import`), which are
    functionally equivalent to the already-denied builtin `__import__` and can load any module as a
    gateway to code execution. Bypassed in `unsafe=True` mode, which disables all safety checks.

View on GitHub (pinned to e318778c9b)

Solutions

  1. Replace the builtin reference in the serialized data with the intended public callable/component.
  2. If the data is trusted and the builtin is genuinely needed, load with unsafe=True.
  3. Quarantine/inspect pipeline files from unknown sources before loading.
  4. Ensure your components serialize callables as fully-qualified, non-builtin names.

Example fix

// before
"callable": "builtins.eval"

// after (trusted data only)
Pipeline.loads(data, unsafe=True)  # or replace with the intended component callable
Defensive patterns

Strategy: try-catch

Validate before calling

import re
DENIED = {"eval", "exec", "open", "__import__", "getattr", "compile", "input"}
def references_denied_builtin(serialized: str) -> bool:
    return any(f"builtins.{n}" in serialized or f".{n}" in serialized for n in DENIED)

Try / catch

from haystack.core.errors import DeserializationError
try:
    pipe = Pipeline.load(path)
except DeserializationError as e:
    if "builtin" in str(e):
        raise  # verify data source before any unsafe=True bypass
raise

Prevention

When it happens

Trigger: Serialized callable strings such as 'builtins.eval', 'builtins.exec', 'builtins.open' appearing in untrusted pipeline data passed to deserialize_callable().

Common situations: Malicious pipeline files shared in the wild; pen-test payloads; mis-serialized configs where a callable was stored as a builtin name instead of a proper component method.

Related errors


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