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

1# SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai> 

2# 

3# SPDX-License-Identifier: Apache-2.0 

4 

5from types import ModuleType 

6from typing import Any 

7 

8from jinja2.sandbox import SandboxedEnvironment 

9 

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) 

35 

36 

37class HaystackSandboxedEnvironment(SandboxedEnvironment): 

38 """ 

39 A `SandboxedEnvironment` hardened against sandbox-escape gadgets. 

40 

41 On top of Jinja2's stock sandbox it additionally: 

42 

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`). 

47 

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 """ 

52 

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) 

59 

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)