| """A sandbox layer that ensures unsafe operations cannot be performed. |
| Useful when the template itself comes from an untrusted source. |
| """ |
| import operator |
| import types |
| import typing as t |
| from _string import formatter_field_name_split # type: ignore |
| from collections import abc |
| from collections import deque |
| from string import Formatter |
| |
| from markupsafe import EscapeFormatter |
| from markupsafe import Markup |
| |
| from .environment import Environment |
| from .exceptions import SecurityError |
| from .runtime import Context |
| from .runtime import Undefined |
| |
| F = t.TypeVar("F", bound=t.Callable[..., t.Any]) |
| |
| #: maximum number of items a range may produce |
| MAX_RANGE = 100000 |
| |
| #: Unsafe function attributes. |
| UNSAFE_FUNCTION_ATTRIBUTES: t.Set[str] = set() |
| |
| #: Unsafe method attributes. Function attributes are unsafe for methods too. |
| UNSAFE_METHOD_ATTRIBUTES: t.Set[str] = set() |
| |
| #: unsafe generator attributes. |
| UNSAFE_GENERATOR_ATTRIBUTES = {"gi_frame", "gi_code"} |
| |
| #: unsafe attributes on coroutines |
| UNSAFE_COROUTINE_ATTRIBUTES = {"cr_frame", "cr_code"} |
| |
| #: unsafe attributes on async generators |
| UNSAFE_ASYNC_GENERATOR_ATTRIBUTES = {"ag_code", "ag_frame"} |
| |
| _mutable_spec: t.Tuple[t.Tuple[t.Type, t.FrozenSet[str]], ...] = ( |
| ( |
| abc.MutableSet, |
| frozenset( |
| [ |
| "add", |
| "clear", |
| "difference_update", |
| "discard", |
| "pop", |
| "remove", |
| "symmetric_difference_update", |
| "update", |
| ] |
| ), |
| ), |
| ( |
| abc.MutableMapping, |
| frozenset(["clear", "pop", "popitem", "setdefault", "update"]), |
| ), |
| ( |
| abc.MutableSequence, |
| frozenset(["append", "reverse", "insert", "sort", "extend", "remove"]), |
| ), |
| ( |
| deque, |
| frozenset( |
| [ |
| "append", |
| "appendleft", |
| "clear", |
| "extend", |
| "extendleft", |
| "pop", |
| "popleft", |
| "remove", |
| "rotate", |
| ] |
| ), |
| ), |
| ) |
| |
| |
| def inspect_format_method(callable: t.Callable) -> t.Optional[str]: |
| if not isinstance( |
| callable, (types.MethodType, types.BuiltinMethodType) |
| ) or callable.__name__ not in ("format", "format_map"): |
| return None |
| |
| obj = callable.__self__ |
| |
| if isinstance(obj, str): |
| return obj |
| |
| return None |
| |
| |
| def safe_range(*args: int) -> range: |
| """A range that can't generate ranges with a length of more than |
| MAX_RANGE items. |
| """ |
| rng = range(*args) |
| |
| if len(rng) > MAX_RANGE: |
| raise OverflowError( |
| "Range too big. The sandbox blocks ranges larger than" |
| f" MAX_RANGE ({MAX_RANGE})." |
| ) |
| |
| return rng |
| |
| |
| def unsafe(f: F) -> F: |
| """Marks a function or method as unsafe. |
| |
| .. code-block: python |
| |
| @unsafe |
| def delete(self): |
| pass |
| """ |
| f.unsafe_callable = True # type: ignore |
| return f |
| |
| |
| def is_internal_attribute(obj: t.Any, attr: str) -> bool: |
| """Test if the attribute given is an internal python attribute. For |
| example this function returns `True` for the `func_code` attribute of |
| python objects. This is useful if the environment method |
| :meth:`~SandboxedEnvironment.is_safe_attribute` is overridden. |
| |
| >>> from jinja2.sandbox import is_internal_attribute |
| >>> is_internal_attribute(str, "mro") |
| True |
| >>> is_internal_attribute(str, "upper") |
| False |
| """ |
| if isinstance(obj, types.FunctionType): |
| if attr in UNSAFE_FUNCTION_ATTRIBUTES: |
| return True |
| elif isinstance(obj, types.MethodType): |
| if attr in UNSAFE_FUNCTION_ATTRIBUTES or attr in UNSAFE_METHOD_ATTRIBUTES: |
| return True |
| elif isinstance(obj, type): |
| if attr == "mro": |
| return True |
| elif isinstance(obj, (types.CodeType, types.TracebackType, types.FrameType)): |
| return True |
| elif isinstance(obj, types.GeneratorType): |
| if attr in UNSAFE_GENERATOR_ATTRIBUTES: |
| return True |
| elif hasattr(types, "CoroutineType") and isinstance(obj, types.CoroutineType): |
| if attr in UNSAFE_COROUTINE_ATTRIBUTES: |
| return True |
| elif hasattr(types, "AsyncGeneratorType") and isinstance( |
| obj, types.AsyncGeneratorType |
| ): |
| if attr in UNSAFE_ASYNC_GENERATOR_ATTRIBUTES: |
| return True |
| return attr.startswith("__") |
| |
| |
| def modifies_known_mutable(obj: t.Any, attr: str) -> bool: |
| """This function checks if an attribute on a builtin mutable object |
| (list, dict, set or deque) or the corresponding ABCs would modify it |
| if called. |
| |
| >>> modifies_known_mutable({}, "clear") |
| True |
| >>> modifies_known_mutable({}, "keys") |
| False |
| >>> modifies_known_mutable([], "append") |
| True |
| >>> modifies_known_mutable([], "index") |
| False |
| |
| If called with an unsupported object, ``False`` is returned. |
| |
| >>> modifies_known_mutable("foo", "upper") |
| False |
| """ |
| for typespec, unsafe in _mutable_spec: |
| if isinstance(obj, typespec): |
| return attr in unsafe |
| return False |
| |
| |
| class SandboxedEnvironment(Environment): |
| """The sandboxed environment. It works like the regular environment but |
| tells the compiler to generate sandboxed code. Additionally subclasses of |
| this environment may override the methods that tell the runtime what |
| attributes or functions are safe to access. |
| |
| If the template tries to access insecure code a :exc:`SecurityError` is |
| raised. However also other exceptions may occur during the rendering so |
| the caller has to ensure that all exceptions are caught. |
| """ |
| |
| sandboxed = True |
| |
| #: default callback table for the binary operators. A copy of this is |
| #: available on each instance of a sandboxed environment as |
| #: :attr:`binop_table` |
| default_binop_table: t.Dict[str, t.Callable[[t.Any, t.Any], t.Any]] = { |
| "+": operator.add, |
| "-": operator.sub, |
| "*": operator.mul, |
| "/": operator.truediv, |
| "//": operator.floordiv, |
| "**": operator.pow, |
| "%": operator.mod, |
| } |
| |
| #: default callback table for the unary operators. A copy of this is |
| #: available on each instance of a sandboxed environment as |
| #: :attr:`unop_table` |
| default_unop_table: t.Dict[str, t.Callable[[t.Any], t.Any]] = { |
| "+": operator.pos, |
| "-": operator.neg, |
| } |
| |
| #: a set of binary operators that should be intercepted. Each operator |
| #: that is added to this set (empty by default) is delegated to the |
| #: :meth:`call_binop` method that will perform the operator. The default |
| #: operator callback is specified by :attr:`binop_table`. |
| #: |
| #: The following binary operators are interceptable: |
| #: ``//``, ``%``, ``+``, ``*``, ``-``, ``/``, and ``**`` |
| #: |
| #: The default operation form the operator table corresponds to the |
| #: builtin function. Intercepted calls are always slower than the native |
| #: operator call, so make sure only to intercept the ones you are |
| #: interested in. |
| #: |
| #: .. versionadded:: 2.6 |
| intercepted_binops: t.FrozenSet[str] = frozenset() |
| |
| #: a set of unary operators that should be intercepted. Each operator |
| #: that is added to this set (empty by default) is delegated to the |
| #: :meth:`call_unop` method that will perform the operator. The default |
| #: operator callback is specified by :attr:`unop_table`. |
| #: |
| #: The following unary operators are interceptable: ``+``, ``-`` |
| #: |
| #: The default operation form the operator table corresponds to the |
| #: builtin function. Intercepted calls are always slower than the native |
| #: operator call, so make sure only to intercept the ones you are |
| #: interested in. |
| #: |
| #: .. versionadded:: 2.6 |
| intercepted_unops: t.FrozenSet[str] = frozenset() |
| |
| def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: |
| super().__init__(*args, **kwargs) |
| self.globals["range"] = safe_range |
| self.binop_table = self.default_binop_table.copy() |
| self.unop_table = self.default_unop_table.copy() |
| |
| def is_safe_attribute(self, obj: t.Any, attr: str, value: t.Any) -> bool: |
| """The sandboxed environment will call this method to check if the |
| attribute of an object is safe to access. Per default all attributes |
| starting with an underscore are considered private as well as the |
| special attributes of internal python objects as returned by the |
| :func:`is_internal_attribute` function. |
| """ |
| return not (attr.startswith("_") or is_internal_attribute(obj, attr)) |
| |
| def is_safe_callable(self, obj: t.Any) -> bool: |
| """Check if an object is safely callable. By default callables |
| are considered safe unless decorated with :func:`unsafe`. |
| |
| This also recognizes the Django convention of setting |
| ``func.alters_data = True``. |
| """ |
| return not ( |
| getattr(obj, "unsafe_callable", False) or getattr(obj, "alters_data", False) |
| ) |
| |
| def call_binop( |
| self, context: Context, operator: str, left: t.Any, right: t.Any |
| ) -> t.Any: |
| """For intercepted binary operator calls (:meth:`intercepted_binops`) |
| this function is executed instead of the builtin operator. This can |
| be used to fine tune the behavior of certain operators. |
| |
| .. versionadded:: 2.6 |
| """ |
| return self.binop_table[operator](left, right) |
| |
| def call_unop(self, context: Context, operator: str, arg: t.Any) -> t.Any: |
| """For intercepted unary operator calls (:meth:`intercepted_unops`) |
| this function is executed instead of the builtin operator. This can |
| be used to fine tune the behavior of certain operators. |
| |
| .. versionadded:: 2.6 |
| """ |
| return self.unop_table[operator](arg) |
| |
| def getitem( |
| self, obj: t.Any, argument: t.Union[str, t.Any] |
| ) -> t.Union[t.Any, Undefined]: |
| """Subscribe an object from sandboxed code.""" |
| try: |
| return obj[argument] |
| except (TypeError, LookupError): |
| if isinstance(argument, str): |
| try: |
| attr = str(argument) |
| except Exception: |
| pass |
| else: |
| try: |
| value = getattr(obj, attr) |
| except AttributeError: |
| pass |
| else: |
| if self.is_safe_attribute(obj, argument, value): |
| return value |
| return self.unsafe_undefined(obj, argument) |
| return self.undefined(obj=obj, name=argument) |
| |
| def getattr(self, obj: t.Any, attribute: str) -> t.Union[t.Any, Undefined]: |
| """Subscribe an object from sandboxed code and prefer the |
| attribute. The attribute passed *must* be a bytestring. |
| """ |
| try: |
| value = getattr(obj, attribute) |
| except AttributeError: |
| try: |
| return obj[attribute] |
| except (TypeError, LookupError): |
| pass |
| else: |
| if self.is_safe_attribute(obj, attribute, value): |
| return value |
| return self.unsafe_undefined(obj, attribute) |
| return self.undefined(obj=obj, name=attribute) |
| |
| def unsafe_undefined(self, obj: t.Any, attribute: str) -> Undefined: |
| """Return an undefined object for unsafe attributes.""" |
| return self.undefined( |
| f"access to attribute {attribute!r} of" |
| f" {type(obj).__name__!r} object is unsafe.", |
| name=attribute, |
| obj=obj, |
| exc=SecurityError, |
| ) |
| |
| def format_string( |
| self, |
| s: str, |
| args: t.Tuple[t.Any, ...], |
| kwargs: t.Dict[str, t.Any], |
| format_func: t.Optional[t.Callable] = None, |
| ) -> str: |
| """If a format call is detected, then this is routed through this |
| method so that our safety sandbox can be used for it. |
| """ |
| formatter: SandboxedFormatter |
| if isinstance(s, Markup): |
| formatter = SandboxedEscapeFormatter(self, escape=s.escape) |
| else: |
| formatter = SandboxedFormatter(self) |
| |
| if format_func is not None and format_func.__name__ == "format_map": |
| if len(args) != 1 or kwargs: |
| raise TypeError( |
| "format_map() takes exactly one argument" |
| f" {len(args) + (kwargs is not None)} given" |
| ) |
| |
| kwargs = args[0] |
| args = () |
| |
| rv = formatter.vformat(s, args, kwargs) |
| return type(s)(rv) |
| |
| def call( |
| __self, # noqa: B902 |
| __context: Context, |
| __obj: t.Any, |
| *args: t.Any, |
| **kwargs: t.Any, |
| ) -> t.Any: |
| """Call an object from sandboxed code.""" |
| fmt = inspect_format_method(__obj) |
| if fmt is not None: |
| return __self.format_string(fmt, args, kwargs, __obj) |
| |
| # the double prefixes are to avoid double keyword argument |
| # errors when proxying the call. |
| if not __self.is_safe_callable(__obj): |
| raise SecurityError(f"{__obj!r} is not safely callable") |
| return __context.call(__obj, *args, **kwargs) |
| |
| |
| class ImmutableSandboxedEnvironment(SandboxedEnvironment): |
| """Works exactly like the regular `SandboxedEnvironment` but does not |
| permit modifications on the builtin mutable objects `list`, `set`, and |
| `dict` by using the :func:`modifies_known_mutable` function. |
| """ |
| |
| def is_safe_attribute(self, obj: t.Any, attr: str, value: t.Any) -> bool: |
| if not super().is_safe_attribute(obj, attr, value): |
| return False |
| |
| return not modifies_known_mutable(obj, attr) |
| |
| |
| class SandboxedFormatter(Formatter): |
| def __init__(self, env: Environment, **kwargs: t.Any) -> None: |
| self._env = env |
| super().__init__(**kwargs) |
| |
| def get_field( |
| self, field_name: str, args: t.Sequence[t.Any], kwargs: t.Mapping[str, t.Any] |
| ) -> t.Tuple[t.Any, str]: |
| first, rest = formatter_field_name_split(field_name) |
| obj = self.get_value(first, args, kwargs) |
| for is_attr, i in rest: |
| if is_attr: |
| obj = self._env.getattr(obj, i) |
| else: |
| obj = self._env.getitem(obj, i) |
| return obj, first |
| |
| |
| class SandboxedEscapeFormatter(SandboxedFormatter, EscapeFormatter): |
| pass |