Spaces:
Running
Running
"""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 collections import abc | |
from collections import deque | |
from string import Formatter | |
from _string import formatter_field_name_split # type: ignore | |
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.Any], 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.Any]) -> 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[..., t.Any]] = 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 | |