Coverage for haystack/utils/jinja2_sandbox.py: 100%
16 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
5from types import ModuleType
6from typing import Any
8from jinja2.sandbox import SandboxedEnvironment
10# Roots of modules whose callables must never be invocable from a template. Reaching one of these
11# from inside a rendered template is only possible after a sandbox escape (e.g. a custom filter that
12# returns a module object), so blocking the call is a defense-in-depth backstop. `builtins` is
13# deliberately excluded: ordinary template operations such as `{{ name.upper() }}` resolve to
14# builtin methods, and blocking those would break legitimate templates.
15_UNSAFE_MODULE_ROOTS: frozenset[str] = frozenset(
16 {
17 "os",
18 "sys",
19 "subprocess",
20 "socket",
21 "shutil",
22 "importlib",
23 "ctypes",
24 "posix",
25 "nt",
26 "pty",
27 "pickle",
28 "shelve",
29 "marshal",
30 "multiprocessing",
31 "code",
32 "pdb",
33 }
34)
37class HaystackSandboxedEnvironment(SandboxedEnvironment):
38 """
39 A `SandboxedEnvironment` hardened against sandbox-escape gadgets.
41 On top of Jinja2's stock sandbox it additionally:
43 - refuses attribute access on module objects, so a module that leaks into the template context
44 (e.g. via a custom filter that imports one) cannot be walked into (`os.system`, ...);
45 - refuses to call module objects, and refuses to call any callable whose defining module is
46 rooted in a dangerous standard-library module (see :data:`_UNSAFE_MODULE_ROOTS`).
48 Note that Jinja invokes *filters* directly, bypassing `is_safe_callable`, so this does not
49 constrain what a registered `custom_filters` function itself does; it only governs attribute
50 access and calls written in template text.
51 """
53 def is_safe_attribute(self, obj: Any, attr: str, value: Any) -> bool:
54 """Reject attribute access on module objects; otherwise defer to the stock sandbox."""
55 # Templates never legitimately reach into a module object's attributes.
56 if isinstance(obj, ModuleType):
57 return False
58 return super().is_safe_attribute(obj, attr, value)
60 def is_safe_callable(self, obj: Any) -> bool:
61 """Reject calling module objects and callables from dangerous modules; else defer to super."""
62 if isinstance(obj, ModuleType):
63 return False
64 root = (getattr(obj, "__module__", "") or "").split(".", 1)[0]
65 if root in _UNSAFE_MODULE_ROOTS:
66 return False
67 return super().is_safe_callable(obj)