Made an application for supporting sustainable local businesses in San Pancho.
Never really got completed, but it has some useful Svelte components for maps that we can reuse.
http://greenspots.dctrl.space
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
428 lines
14 KiB
428 lines
14 KiB
"""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) # type: ignore |
|
|
|
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
|
|
|