Coverage for haystack/core/serialization_security.py: 100%
135 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 13:53 +0000
1# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai>
2#
3# SPDX-License-Identifier: Apache-2.0
5"""
6Security primitives for pipeline deserialization.
8This module provides an allowlist mechanism that gates arbitrary imports.
10Three ways to extend the allowlist:
11- Per-call kwarg: `Pipeline.load(..., allowed_modules=["mypkg.*"])`
12- Process-wide programmatic API: :func:`allow_deserialization_module`
13- Environment variable: `HAYSTACK_DESERIALIZATION_ALLOWLIST="mypkg.*,otherpkg.*"`
15The two-mode loading API (`unsafe=True`) bypasses the allowlist entirely. For deployments that only
16ever load fully trusted pipelines and cannot pass `unsafe=True` at every call site, the process-wide
17environment variable `HAYSTACK_UNSAFE_DESERIALIZATION=1` is equivalent to `unsafe=True` on every load:
18it disables *all* deserialization safety checks (the module allowlist, the builtin/import-primitive
19and control-plane denylists, the object-internals traversal guard, and the refusal to honor a
20component's own `unsafe: true` flag). Only enable it when every pipeline loaded by the process is
21trusted. Its value is read once, on the first deserialization in the process, and then frozen for the
22process lifetime, so nothing that runs later can turn the safety checks off (or back on).
23"""
25import builtins
26import contextvars
27import fnmatch
28import importlib
29import os
30from collections.abc import Callable, Iterable, Iterator
31from contextlib import contextmanager
32from dataclasses import dataclass, field
33from types import ModuleType
34from typing import TypeVar
36from haystack import logging
37from haystack.core.errors import DeserializationError
39# The default allowlist covers Haystack's own packages plus a small set of standard-library type modules
40# that are commonly referenced in serialized type annotations (e.g. `typing.List[str]`,
41# `collections.deque`). Importing these modules has no meaningful side effects on its own.
42DEFAULT_ALLOWED_MODULES: tuple[str, ...] = (
43 "haystack",
44 "haystack_integrations",
45 "haystack_experimental",
46 "builtins",
47 "typing",
48 "collections",
49)
50DESERIALIZATION_ALLOWLIST_ENV_VAR = "HAYSTACK_DESERIALIZATION_ALLOWLIST"
52# Process-wide "off switch": when set to a truthy value, deserialization behaves as if every load
53# were called with `unsafe=True` (see `_is_unsafe_deserialization`). Unlike the allowlist env var,
54# this disables *all* safety checks, so it must only be used when every pipeline the process loads
55# is trusted.
56UNSAFE_DESERIALIZATION_ENV_VAR = "HAYSTACK_UNSAFE_DESERIALIZATION"
58# `builtins` is on the default allowlist because deserialization legitimately needs builtin *types*
59# (e.g. `builtins.str`, used in serialized type annotations and as nested `{"type": ...}` class
60# references) and harmless builtin callables that Haystack's own serializer emits (e.g.
61# `serialize_callable(print)` -> `"builtins.print"`). The module-granular allowlist is too coarse
62# to separate those from dangerous members, so the two builtin-resolving contexts are gated
63# differently:
64# - Type / class contexts (`deserialize_type`, `import_class_by_name`) require the resolved
65# builtin to be a `type` (see :func:`_check_builtin_is_type`). That lets every builtin type
66# through while rejecting every builtin *function*, with no denylist to maintain.
67# - The callable context (`deserialize_callable`) genuinely returns functions, so it instead
68# rejects the dangerous builtin *callables* named below (see :func:`_check_not_denied_builtin`).
69_DENIED_BUILTIN_NAMES: frozenset[str] = frozenset(
70 {
71 "eval", # arbitrary code execution
72 "exec", # arbitrary code execution
73 "compile", # arbitrary code compilation
74 "__import__", # dynamic import of any module (gateway to os/subprocess/...)
75 "open", # filesystem read/write
76 "getattr", # attribute-traversal gadget (classic sandbox escape)
77 "setattr", # arbitrary attribute mutation
78 "delattr", # arbitrary attribute deletion
79 "globals", # access to module namespaces
80 "locals", # access to local namespaces
81 "vars", # access to object/module namespaces
82 "breakpoint", # runs the PYTHONBREAKPOINT hook
83 "__build_class__", # dynamic class creation
84 "type", # dynamic class creation via type(name, bases, dict)
85 }
86)
88# Resolve names to objects once so callers can match by identity, which also catches aliases that
89# reach the same builtin via a different import path (e.g. `io.open is builtins.open`).
90_DENIED_BUILTIN_OBJECTS: frozenset = frozenset([getattr(builtins, name) for name in _DENIED_BUILTIN_NAMES])
92# Import primitives are functional twins of the denied builtin `__import__`: they load an arbitrary
93# module by name (the gateway to `os`/`subprocess`/...). The module-granular allowlist does not stop
94# them, because a wrapper like `haystack.utils.type_serialization.thread_safe_import` lives inside
95# the allowlisted `haystack` namespace and reports `__module__ == "haystack..."`, and the builtin
96# denylist above only matches `builtins` members by identity. Deny these import primitives too.
97_DENIED_CALLABLE_OBJECTS: frozenset = frozenset({importlib.import_module, importlib.reload})
98# `thread_safe_import` is matched by its (`__module__`, `__qualname__`) pair rather than by identity
99# so this module need not import `haystack.utils.type_serialization` (which itself imports from this
100# module, so an import here would be circular).
101_DENIED_CALLABLE_QUALNAMES: frozenset[tuple[str, str]] = frozenset(
102 {("haystack.utils.type_serialization", "thread_safe_import")}
103)
105logger = logging.getLogger(__name__)
108@dataclass(frozen=True)
109class _DeserializationContext:
110 extra_allowed: tuple[str, ...] = field(default_factory=tuple)
111 unsafe: bool = False
114_current_context: contextvars.ContextVar[_DeserializationContext | None] = contextvars.ContextVar(
115 "haystack_deserialization_context", default=None
116)
119def _get_context() -> _DeserializationContext:
120 ctx = _current_context.get()
121 return ctx if ctx is not None else _DeserializationContext()
124# Snapshot of `UNSAFE_DESERIALIZATION_ENV_VAR`, read once and then frozen for the process lifetime
125# (`None` means "not read yet"). Freezing it is a security property, not an optimization: the first
126# read happens before any deserialized data can run, so a hostile pipeline cannot turn the safety
127# checks off *while it is being loaded*. Without it, any route from serialized data to `os.environ`
128# is a full bypass — e.g. an allowlisted module that binds `os.environ` at module scope, whose
129# `update` resolves because it is `collections.abc.MutableMapping.update` and `collections` is on
130# the default allowlist. The read is deliberately lazy rather than done at import time, so that a
131# `load_dotenv()` (or any other env setup) that runs before the first load is still honored.
132_unsafe_env_snapshot: bool | None = None
135def _unsafe_env_enabled() -> bool:
136 """
137 Return whether the process-wide unsafe-deserialization env var was set to a truthy value.
139 :data:`UNSAFE_DESERIALIZATION_ENV_VAR` is read on the first deserialization check in the process
140 and the result is then frozen for the process lifetime (see `_unsafe_env_snapshot`): later writes
141 to the variable are ignored, in either direction. A warning is logged when the snapshot is taken
142 and found active, since it turns off all deserialization safety for the whole process.
143 """
144 global _unsafe_env_snapshot
145 snapshot = _unsafe_env_snapshot
146 if snapshot is None:
147 # A benign race: concurrent first readers compute the same value from the same environment.
148 snapshot = os.environ.get(UNSAFE_DESERIALIZATION_ENV_VAR, "").strip().lower() in ("1", "true")
149 _unsafe_env_snapshot = snapshot
150 if snapshot:
151 logger.warning(
152 "{env} is set: pipeline deserialization safety is DISABLED process-wide "
153 "(equivalent to passing unsafe=True on every load). Only enable this if every "
154 "pipeline loaded by this process is fully trusted.",
155 env=UNSAFE_DESERIALIZATION_ENV_VAR,
156 )
157 return snapshot
160def _is_unsafe_deserialization() -> bool:
161 """
162 Return whether deserialization is running in unsafe mode.
164 This is the single source of truth consulted by every safety check in this module. It is `True`
165 when either the active deserialization context was entered with `unsafe=True`, or the process-wide
166 :data:`UNSAFE_DESERIALIZATION_ENV_VAR` env var was set when it was first read (which disables all
167 deserialization safety checks for the whole process — see :func:`_unsafe_env_enabled`).
169 Components deserializing their own data (e.g. `OutputAdapter.from_dict`) use this to decide
170 whether to honor an embedded `unsafe` flag: a serialized component may only disable its Jinja
171 sandbox when the whole pipeline is being loaded in unsafe mode (`Pipeline.load(..., unsafe=True)`),
172 never on its own from otherwise-untrusted data in default safe mode.
173 """
174 # `_unsafe_env_enabled()` first so the env snapshot is always taken on the earliest
175 # check in the process, even when that check happens inside an `unsafe=True` load.
176 return _unsafe_env_enabled() or _get_context().unsafe
179_F = TypeVar("_F", bound=Callable[..., object])
181# Attribute stamped on callables that are part of the deserializer's own machinery so that the
182# resolution paths can refuse to hand them back (see `mark_deserialization_internal`).
183_DESERIALIZATION_INTERNAL_ATTR = "_haystack_deserialization_internal"
186def mark_deserialization_internal(func: _F) -> _F:
187 """
188 Mark a callable as deserializer-internal so it can never be produced by deserializing untrusted data.
190 The allowlist admits the whole `haystack` namespace so Haystack can deserialize its own
191 components. That also makes the deserializer's *own* interface resolvable from serialized data:
192 the allowlist-administration function :func:`allow_deserialization_module` and the resolution
193 helpers (`deserialize_callable`, `deserialize_type`, `import_class_by_name`).
194 A hostile pipeline can register `allow_deserialization_module` as a Jinja custom filter, call it
195 with `"*"` to disarm the allowlist process-wide, then use the equally-resolvable `deserialize_callable`
196 to resolve and invoke `os.system`, for example.
198 Stamp such callables at definition time with this decorator; the resolution paths
199 (:func:`deserialize_callable`, `_import_class_by_name`) refuse to return anything carrying the
200 mark. Bypassed in `unsafe=True` mode, which disables all deserialization safety checks by design.
202 :param func:
203 The callable to mark.
204 :returns:
205 The same callable, marked.
206 """
207 setattr(func, _DESERIALIZATION_INTERNAL_ATTR, True)
208 return func
211def _is_deserialization_internal(resolved: object) -> bool:
212 """
213 Return whether `resolved` belongs to Haystack's deserialization control plane.
215 An object belongs to it in any of three ways:
217 - It is stamped with :func:`mark_deserialization_internal`. This covers the resolution helpers
218 that live in *other* modules (`deserialize_callable`, `deserialize_type`, `import_class_by_name`).
219 - It is defined in this module (matched by `__module__`): `allow_deserialization_module` and the
220 private context machinery (`_DeserializationContext`, the `_check_*`/`_get_context` helpers).
221 - It is a bound method of the module-level *mutable* control-plane state — the allowlist list
222 `_extra_allowed_modules` and the `_current_context` context variable. These are reachable
223 through the very attribute walk the resolver performs (e.g. `_extra_allowed_modules.append`,
224 `_current_context.set`); their own `__module__` is `builtins`/`None`, so they are matched
225 by the identity of what they are bound to. Left reachable, they let serialized data append
226 `"*"` to the allowlist or install an `unsafe` context — operating the control from within
227 the very data it exists to distrust, which persists process-wide and enables a staged RCE on a
228 later load.
229 """
230 if getattr(resolved, _DESERIALIZATION_INTERNAL_ATTR, False):
231 return True
232 if getattr(resolved, "__module__", None) == __name__:
233 return True
234 state = (_extra_allowed_modules, _current_context)
235 bound_to = getattr(resolved, "__self__", None)
236 return any(resolved is s or bound_to is s for s in state)
239def _check_not_deserialization_internal(resolved: object, handle: str) -> None:
240 """
241 Reject `resolved` if it is part of the deserialization control plane.
243 See :func:`_is_deserialization_internal` for what that covers.
244 Used by the resolution paths (`deserialize_callable`, `_import_class_by_name`) as a companion to
245 the builtin and import-primitive denylists. It refuses the allowlist-administration function, the
246 resolution helpers, and the mutable allowlist/context state — all of which live in (or are
247 reachable through) the allowlisted `haystack` namespace and would otherwise be resolvable from
248 serialized data. Bypassed in `unsafe=True` mode, which disables all safety checks.
250 :param resolved:
251 The object resolved from the serialized handle.
252 :param handle:
253 The original serialized handle, used only for the error message.
254 :raises DeserializationError:
255 If `resolved` is part of the deserialization control plane.
256 """
257 if _is_unsafe_deserialization():
258 return
259 if _is_deserialization_internal(resolved):
260 name = getattr(resolved, "__qualname__", None) or getattr(resolved, "__name__", None) or repr(resolved)
261 raise DeserializationError(
262 f"Refusing to deserialize '{handle}': it resolves to '{name}', which is part of Haystack's "
263 f"deserialization control plane (its allowlist administration, mutable allowlist/context state, "
264 f"or a resolution helper) and must never be produced by deserializing untrusted data — doing so "
265 f"would let the data operate the deserialization allowlist against itself. If you trust the "
266 f"source of this data, load it with unsafe=True to bypass deserialization safety checks."
267 )
270# Non-dunder attribute names that still expose an object's internals — the frame/code/closure
271# accessors on functions, generators, coroutines and async generators. Dunder names (`__globals__`,
272# `__dict__`, `__class__`, `__builtins__`, `__subclasses__`, ...) are matched separately by the
273# `__` prefix; these have no such prefix and must be listed explicitly.
274_UNSAFE_TRAVERSAL_ATTRS: frozenset[str] = frozenset(
275 {
276 "gi_frame",
277 "gi_code",
278 "gi_yieldfrom",
279 "cr_frame",
280 "cr_code",
281 "cr_await",
282 "ag_frame",
283 "ag_code",
284 "f_globals",
285 "f_builtins",
286 "f_locals",
287 "f_back",
288 "f_code",
289 "func_globals",
290 "func_code",
291 "func_closure",
292 "func_dict",
293 "func_defaults",
294 }
295)
298def _check_traversable_attribute(name: str, handle: str) -> None:
299 """
300 Reject descending into an object-internals attribute while walking a serialized handle.
302 Serialized callable/class handles reference public dotted import paths (`module.Class.method`);
303 they never legitimately traverse into an object's internals. Dunder attributes (`__globals__`,
304 `__dict__`, `__class__`, `__builtins__`, `__subclasses__`, ...) and the frame/code accessors in
305 :data:`_UNSAFE_TRAVERSAL_ATTRS` are the classic sandbox-escape gadgets — e.g. `<func>.__globals__`
306 yields the defining module's live namespace, from which the allowlist state can be rewritten or
307 `__builtins__` (hence `eval`/`exec`) reached, regardless of any per-object identity check. The
308 module-granular allowlist does not stop this because the traversal stays inside an allowlisted
309 module. Bypassed in `unsafe=True` mode, which disables all deserialization safety checks by design.
311 :param name:
312 The attribute name about to be resolved from the current object in the walk.
313 :param handle:
314 The original serialized handle, used only for the error message.
315 :raises DeserializationError:
316 If `name` names an object-internals attribute.
317 """
318 if _is_unsafe_deserialization():
319 return
320 if name.startswith("__") or name in _UNSAFE_TRAVERSAL_ATTRS:
321 raise DeserializationError(
322 f"Refusing to deserialize '{handle}': it traverses into the internal attribute '{name}', "
323 f"which can expose object internals (e.g. '__globals__', '__class__', '__builtins__') and is "
324 f"a known sandbox-escape gadget. If you trust the source of this data, load it with unsafe=True "
325 f"to bypass deserialization safety checks."
326 )
329# Process-wide patterns set via allow_deserialization_module.
330_extra_allowed_modules: list[str] = []
333@mark_deserialization_internal
334def allow_deserialization_module(pattern: str) -> None:
335 """
336 Add a module pattern to the process-wide deserialization allowlist.
338 Once added, classes from modules matching the pattern can be deserialized from YAML / dict
339 representations until the process exits.
341 A pattern matches a module name if:
342 - The pattern contains `*`, `?` or `[` — :mod:`fnmatch` semantics are used.
343 - Otherwise the pattern is treated as a prefix: a module matches if it equals the pattern or
344 is a submodule of it (i.e. starts with `pattern + "."`). A trailing `.*` is stripped
345 before this comparison, so `"mypkg"` and `"mypkg.*"` behave identically.
347 :param pattern:
348 The module pattern to allow.
349 """
350 if pattern not in _extra_allowed_modules:
351 _extra_allowed_modules.append(pattern)
354def _module_matches(module_name: str, pattern: str) -> bool:
355 """Return whether `module_name` matches the given allowlist `pattern`."""
356 # `pkg.*` (where the part before `.*` has no other wildcards) is treated as a prefix match —
357 # matches `pkg` and any submodule. This is the most common form, and we want it to match
358 # the bare top-level package too (which true fnmatch wouldn't, since `pkg.*` requires a
359 # literal `.` to follow). Patterns like `j*on.*` keep their wildcards and fall through to
360 # fnmatch so the semantics stay consistent.
361 if pattern.endswith(".*") and not any(c in pattern[:-2] for c in "*?["):
362 prefix = pattern[:-2]
363 return module_name == prefix or module_name.startswith(prefix + ".")
364 if any(c in pattern for c in "*?["):
365 return fnmatch.fnmatchcase(module_name, pattern)
366 return module_name == pattern or module_name.startswith(pattern + ".")
369def _patterns_from_env() -> list[str]:
370 raw = os.environ.get(DESERIALIZATION_ALLOWLIST_ENV_VAR, "")
371 return [p.strip() for p in raw.split(",") if p.strip()]
374def _is_module_allowed(module_name: str) -> bool:
375 """Return whether `module_name` is on the active deserialization allowlist."""
376 ctx = _get_context()
377 if _is_unsafe_deserialization():
378 return True
379 patterns: list[str] = []
380 patterns.extend(DEFAULT_ALLOWED_MODULES)
381 patterns.extend(_extra_allowed_modules)
382 patterns.extend(_patterns_from_env())
383 patterns.extend(ctx.extra_allowed)
384 return any(_module_matches(module_name, p) for p in patterns)
387def _check_module_allowed(module_name: str) -> None:
388 """Raise :class:`DeserializationError` if `module_name` is not on the allowlist."""
389 if _is_module_allowed(module_name):
390 return
391 raise DeserializationError(
392 f"Refusing to deserialize a class from module '{module_name}': the module is not on the "
393 f"trusted-module allowlist. If you trust the source of this serialized data, you can either:\n"
394 f" - extend the allowlist for this call: "
395 f"Pipeline.load(..., allowed_modules=['{module_name}']),\n"
396 f" - extend it process-wide via haystack.core.serialization.allow_deserialization_module"
397 f"('{module_name}') or the {DESERIALIZATION_ALLOWLIST_ENV_VAR} environment variable,\n"
398 f" - or bypass the allowlist entirely: Pipeline.load(..., unsafe=True)."
399 )
402def _check_resolved_module_allowed(resolved: object, declared_module: str | None = None) -> None:
403 """
404 Gate on the module a resolved object *actually* comes from, not on the declared handle.
406 The allowlist checks that run earlier in deserialization apply to the declared dotted path,
407 which an attacker controls. A crafted handle can name an object re-exported as an attribute of
408 an allowlisted module and thereby escape the allowlist:
410 - a module: ``haystack.utils.auth.os`` resolves to the standard-library ``os`` module, because
411 ``haystack.utils.auth`` does ``import os`` at module scope. The handle has the allowlisted
412 ``haystack`` prefix, but the resolved object belongs to the un-allowlisted ``os`` module.
413 - a class or function: ``haystack.<mod>.<Name>`` could resolve a class/function imported into
414 an allowlisted module from an un-allowlisted one (e.g. a re-exported ``subprocess.Popen``).
416 Checking the resolved object's real module closes both gaps. Objects without a usable
417 ``__module__`` (e.g. ``functools.partial`` instances) are left to the other gates; they are not
418 reachable as gadgets through an attribute walk that stays entirely inside allowlisted modules.
420 :param resolved:
421 The object resolved from the serialized handle.
422 :param declared_module:
423 The allowlisted module the object was actually resolved from (the module that was imported
424 and walked). Used to accept the private implementation module that backs a public module —
425 see below.
426 :raises DeserializationError:
427 If the resolved object's real module is not on the allowlist.
428 """
429 if _is_unsafe_deserialization():
430 return
431 # Builtins are gated separately and authoritatively — by the identity denylist
432 # (`_check_not_denied_builtin`) in the callable path and by the type requirement
433 # (`_check_builtin_is_type`) in the class path. They can also report a private `__module__`
434 # (e.g. `open.__module__ == "_io"`), so exempt any object that is a genuine builtin (reachable
435 # under its own name in the `builtins` module) from the module check to avoid shadowing those
436 # gates. This exemption cannot widen access: a dangerous builtin is still stopped downstream.
437 name = getattr(resolved, "__name__", None)
438 if isinstance(name, str) and getattr(builtins, name, None) is resolved:
439 return
440 # Modules carry their identity in `__name__`, not `__module__`.
441 module_name = resolved.__name__ if isinstance(resolved, ModuleType) else getattr(resolved, "__module__", None)
442 if not (isinstance(module_name, str) and module_name):
443 return
444 if _is_module_allowed(module_name):
445 return
446 # Private implementation module backing an allowlisted public module: a symbol legitimately
447 # exposed by an allowlisted module can report a private `__module__` — e.g. `operator.add`
448 # resolves to `_operator.add`, `io.StringIO` to `_io.StringIO`. Accept it only when the private
449 # module is the C accelerator of the *same* allowlisted module the object was resolved from
450 # (`_operator` backs `operator`, `_io` backs `io`). This never admits a public, un-allowlisted
451 # module such as `os` or `subprocess`, so the escape gadgets stay blocked.
452 if (
453 declared_module is not None
454 and module_name.lstrip("_") == declared_module
455 and _is_module_allowed(declared_module)
456 ):
457 return
458 _check_module_allowed(module_name)
461def _is_denied_builtin(resolved: object) -> bool:
462 """
463 Return whether `resolved` is one of the builtins denied for callable deserialization.
465 Matches by identity (not membership) so an unhashable resolved object never raises.
466 """
467 return any(resolved is denied for denied in _DENIED_BUILTIN_OBJECTS)
470def _check_not_denied_builtin(resolved: object, handle: str) -> None:
471 """
472 Reject `resolved` if it is a builtin callable that is unsafe to resolve from serialized data.
474 Used by the callable-resolution path (`deserialize_callable`). Raises
475 :class:`DeserializationError` for the primitives in :data:`_DENIED_BUILTIN_NAMES`, which can
476 execute code, import modules, touch the filesystem, or escape via attribute/namespace access.
477 The block applies even though `builtins` is on the allowlist, because the allowlist is
478 module-granular. It is intentionally bypassed in `unsafe=True` mode, which disables all
479 deserialization safety checks by design.
481 :param resolved:
482 The object resolved from the serialized handle.
483 :param handle:
484 The original serialized handle, used only for the error message.
485 """
486 if _is_unsafe_deserialization():
487 return
488 if _is_denied_builtin(resolved):
489 name = getattr(resolved, "__name__", str(resolved))
490 raise DeserializationError(
491 f"Refusing to deserialize '{handle}': it resolves to the builtin '{name}', which is "
492 f"blocked because it can be used to execute code, import modules, access the "
493 f"filesystem, or escape via attribute access. If you trust the source of this data, "
494 f"load it with unsafe=True to bypass deserialization safety checks."
495 )
498def _check_not_denied_callable(resolved: object, handle: str) -> None:
499 """
500 Reject `resolved` if it is an import primitive that is unsafe to resolve from serialized data.
502 Used by the callable-resolution path (`deserialize_callable`) as a companion to
503 :func:`_check_not_denied_builtin`. It blocks the non-builtin import primitives in
504 :data:`_DENIED_CALLABLE_OBJECTS` / :data:`_DENIED_CALLABLE_QUALNAMES` (e.g.
505 `importlib.import_module`, `haystack.utils.type_serialization.thread_safe_import`), which are
506 functionally equivalent to the already-denied builtin `__import__` and can load any module as a
507 gateway to code execution. Bypassed in `unsafe=True` mode, which disables all safety checks.
509 :param resolved:
510 The object resolved from the serialized handle.
511 :param handle:
512 The original serialized handle, used only for the error message.
513 """
514 if _is_unsafe_deserialization():
515 return
516 ident = (getattr(resolved, "__module__", ""), getattr(resolved, "__qualname__", ""))
517 if any(resolved is denied for denied in _DENIED_CALLABLE_OBJECTS) or ident in _DENIED_CALLABLE_QUALNAMES:
518 raise DeserializationError(
519 f"Refusing to deserialize '{handle}': it resolves to an import primitive that can load "
520 f"arbitrary modules (equivalent to the blocked builtin '__import__'), which is a gateway "
521 f"to code execution. If you trust the source of this data, load it with unsafe=True to "
522 f"bypass deserialization safety checks."
523 )
526def _check_builtin_is_type(resolved: object, handle: str) -> None:
527 """
528 Reject a `builtins` member resolved in a type/class context that is not a `type`.
530 Used by `deserialize_type` and `import_class_by_name`, which resolve type annotations and class
531 references — always classes. Requiring the resolved `builtins` member to be a `type` lets every
532 builtin type through (e.g. `str`, `memoryview`) while rejecting every builtin *function* (e.g.
533 `eval`, `exec`, `getattr`), with no denylist to maintain. Bypassed in `unsafe=True` mode.
535 :param resolved:
536 The object resolved from the serialized handle.
537 :param handle:
538 The original serialized handle, used only for the error message.
539 """
540 if _is_unsafe_deserialization():
541 return
542 if not isinstance(resolved, type):
543 raise DeserializationError(
544 f"Refusing to deserialize '{handle}': it resolves to a builtin that is not a type and "
545 f"cannot be used as a type annotation or class reference. If you trust the source of "
546 f"this data, load it with unsafe=True to bypass deserialization safety checks."
547 )
550@contextmanager
551def _deserialization_context(allowed_modules: Iterable[str] | None = None, unsafe: bool = False) -> Iterator[None]:
552 """
553 Context manager that activates a per-call deserialization context.
555 Patterns from `allowed_modules` are appended to the parent context's patterns, and `unsafe`
556 is OR-ed with the parent's `unsafe` flag — so this never narrows the active permissions.
557 The previous context is restored on exit.
558 """
559 parent = _get_context()
560 extra = parent.extra_allowed + (tuple(allowed_modules) if allowed_modules else ())
561 merged_unsafe = parent.unsafe or unsafe
562 token = _current_context.set(_DeserializationContext(extra_allowed=extra, unsafe=merged_unsafe))
563 try:
564 yield
565 finally:
566 _current_context.reset(token)