{"record":{"id":"565af277c3d640aa","repo":"deepset-ai/haystack","slug":"refusing-to-deserialize-handle-it-traverses-i","errorCode":null,"errorMessage":"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.","messagePattern":"Refusing to deserialize '(.+?)': it traverses into the internal attribute '(.+?)', 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\\.","errorType":"exception","errorClass":"DeserializationError","httpStatus":null,"severity":"critical","filePath":"haystack/core/serialization_security.py","lineNumber":321,"sourceCode":"    they never legitimately traverse into an object's internals. Dunder attributes (`__globals__`,\n    `__dict__`, `__class__`, `__builtins__`, `__subclasses__`, ...) and the frame/code accessors in\n    :data:`_UNSAFE_TRAVERSAL_ATTRS` are the classic sandbox-escape gadgets — e.g. `<func>.__globals__`\n    yields the defining module's live namespace, from which the allowlist state can be rewritten or\n    `__builtins__` (hence `eval`/`exec`) reached, regardless of any per-object identity check. The\n    module-granular allowlist does not stop this because the traversal stays inside an allowlisted\n    module. Bypassed in `unsafe=True` mode, which disables all deserialization safety checks by design.\n\n    :param name:\n        The attribute name about to be resolved from the current object in the walk.\n    :param handle:\n        The original serialized handle, used only for the error message.\n    :raises DeserializationError:\n        If `name` names an object-internals attribute.\n    \"\"\"\n    if _is_unsafe_deserialization():\n        return\n    if name.startswith(\"__\") or name in _UNSAFE_TRAVERSAL_ATTRS:\n        raise DeserializationError(\n            f\"Refusing to deserialize '{handle}': it traverses into the internal attribute '{name}', \"\n            f\"which can expose object internals (e.g. '__globals__', '__class__', '__builtins__') and is \"\n            f\"a known sandbox-escape gadget. If you trust the source of this data, load it with unsafe=True \"\n            f\"to bypass deserialization safety checks.\"\n        )\n\n\n# Process-wide patterns set via allow_deserialization_module.\n_extra_allowed_modules: list[str] = []\n\n\n@mark_deserialization_internal\ndef allow_deserialization_module(pattern: str) -> None:\n    \"\"\"\n    Add a module pattern to the process-wide deserialization allowlist.\n\n    Once added, classes from modules matching the pattern can be deserialized from YAML / dict\n    representations until the process exits.","sourceCodeStart":303,"sourceCodeEnd":339,"githubUrl":"https://github.com/deepset-ai/haystack/blob/e318778c9bf60a1963e3b5f451359655dd696c30/haystack/core/serialization_security.py#L303-L339","documentation":"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.","triggerScenarios":"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.","commonSituations":"Maliciously crafted pipeline files; accidentally serialized expressions like 'obj.__class__.from_dict'; copying handles from non-Haystack serialized data into pipeline configs.","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."],"exampleFix":"// before\n\"callable\": \"my_module.MyComp.__class__.__init__\"\n\n// after\n\"callable\": \"my_module.MyComp\"","handlingStrategy":"validation","validationCode":"def contains_dunder_traversal(serialized: str) -> bool:\n    import re\n    return bool(re.search(r\"__\\w+__\", serialized))","typeGuard":"def is_safe_handle(handle: str) -> bool:\n    return not any(part.startswith(\"__\") for part in handle.split(\".\"))","tryCatchPattern":"from haystack.core.errors import DeserializationError\ntry:\n    pipe = Pipeline.load(path)\nexcept DeserializationError as e:\n    if \"sandbox-escape\" in str(e) or \"internal attribute\" in str(e):\n        raise  # only bypass with unsafe=True if source is fully trusted\nraise","preventionTips":["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"],"tags":["security","deserialization","sandbox-escape"],"backgroundTag":"dunder-attribute-traversal-blocked","analyzedSha":"e318778c9bf60a1963e3b5f451359655dd696c30","analyzedAt":"2026-08-30T11:45:20.711Z","schemaVersion":2},"datasetVersion":"2026-08-30T13:17:10.514Z"}