Headline
CVE-2023-25657: Sandbox — Jinja Documentation (3.0.x)
Nautobot is a Network Source of Truth and Network Automation Platform. All users of Nautobot versions earlier than 1.5.7 are impacted by a remote code execution vulnerability. Nautobot did not properly sandbox Jinja2 template rendering. In Nautobot 1.5.7 has enabled sandboxed environments for the Jinja2 template engine used internally for template rendering for the following objects: extras.ComputedField
, extras.CustomLink
, extras.ExportTemplate
, extras.Secret
, extras.Webhook
. While no active exploits of this vulnerability are known this change has been made as a preventative measure to protect against any potential remote code execution attacks utilizing maliciously crafted template code. This change forces the Jinja2 template engine to use a SandboxedEnvironment
on all new installations of Nautobot. This addresses any potential unsafe code execution everywhere the helper function nautobot.utilities.utils.render_jinja2
is called. Additionally, the documentation that had previously suggesting the direct use of jinja2.Template
has been revised to suggest render_jinja2
. Users are advised to upgrade to Nautobot 1.5.7 or newer. For users that are unable to upgrade to the latest release of Nautobot, you may add the following setting to your nautobot_config.py
to apply the sandbox environment enforcement: TEMPLATES[1]["OPTIONS"]["environment"] = "jinja2.sandbox.SandboxedEnvironment"
After applying this change, you must restart all Nautobot services, including any Celery worker processes. Note: Nautobot specifies two template engines by default, the first being “django” for the Django built-in template engine, and the second being “jinja” for the Jinja2 template engine. This recommended setting will update the second item in the list of template engines, which is the Jinja2 engine. For users that are unable to immediately update their configuration such as if a Nautobot service restart is too disruptive to operations, access to provide custom Jinja2 template values may be mitigated using permissions to restrict “change” (write) actions to the affected object types listed in the first section. Note: This solution is intended to be stopgap until you can successfully update your nautobot_config.py
or upgrade your Nautobot instance to apply the sandboxed environment enforcement.
The Jinja sandbox can be used to evaluate untrusted code. Access to unsafe attributes and methods is prohibited.
Assuming env is a SandboxedEnvironment in the default configuration the following piece of code shows how it works:
>>> env.from_string(“{{ func.func_code }}”).render(func=lambda:None) u’’ >>> env.from_string(“{{ func.func_code.do_something }}”).render(func=lambda:None) Traceback (most recent call last): … SecurityError: access to attribute ‘func_code’ of ‘function’ object is unsafe.
API¶
class jinja2.sandbox.SandboxedEnvironment([options])¶
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 SecurityError is raised. However also other exceptions may occur during the rendering so the caller has to ensure that all exceptions are caught.
Parameters
args (Any) –
kwargs (Any) –
Return type
None
call_binop(context, operator, left, right)¶
For intercepted binary operator calls (intercepted_binops()) this function is executed instead of the builtin operator. This can be used to fine tune the behavior of certain operators.
Changelog
New in version 2.6.
Parameters
context (jinja2.runtime.Context) –
operator (str) –
left (Any) –
right (Any) –
Return type
Any
call_unop(context, operator, arg)¶
For intercepted unary operator calls (intercepted_unops()) this function is executed instead of the builtin operator. This can be used to fine tune the behavior of certain operators.
Changelog
New in version 2.6.
Parameters
context (jinja2.runtime.Context) –
operator (str) –
arg (Any) –
Return type
Any
default_binop_table_: Dict[str, Callable[[Any, Any], Any]]_ = {’%’: <built-in function mod>, '*’: <built-in function mul>, '**’: <built-in function pow>, '+’: <built-in function add>, '-': <built-in function sub>, '/’: <built-in function truediv>, '//’: <built-in function floordiv>}¶
default callback table for the binary operators. A copy of this is available on each instance of a sandboxed environment as binop_table
default_unop_table_: Dict[str, Callable[[Any], Any]]_ = {’+’: <built-in function pos>, '-': <built-in function neg>}¶
default callback table for the unary operators. A copy of this is available on each instance of a sandboxed environment as unop_table
intercepted_binops_: FrozenSet[str]_ = frozenset({})¶
a set of binary operators that should be intercepted. Each operator that is added to this set (empty by default) is delegated to the call_binop() method that will perform the operator. The default operator callback is specified by 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.
Changelog
New in version 2.6.
intercepted_unops_: 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 call_unop() method that will perform the operator. The default operator callback is specified by 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.
Changelog
New in version 2.6.
is_safe_attribute(obj, attr, value)¶
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 is_internal_attribute() function.
Parameters
obj (Any) –
attr (str) –
value (Any) –
Return type
bool
is_safe_callable(obj)¶
Check if an object is safely callable. By default callables are considered safe unless decorated with unsafe().
This also recognizes the Django convention of setting func.alters_data = True.
Parameters
obj (Any) –
Return type
bool
class jinja2.sandbox.ImmutableSandboxedEnvironment([options])¶
Works exactly like the regular SandboxedEnvironment but does not permit modifications on the builtin mutable objects list, set, and dict by using the modifies_known_mutable() function.
Parameters
args (Any) –
kwargs (Any) –
Return type
None
exception jinja2.sandbox.SecurityError(message=None)¶
Raised if a template tries to do something insecure if the sandbox is enabled.
Parameters
message (Optional[str]) –
Return type
None
jinja2.sandbox.unsafe(f)¶
Marks a function or method as unsafe.
Parameters
f (jinja2.sandbox.F) –
Return type
jinja2.sandbox.F
jinja2.sandbox.is_internal_attribute(obj, attr)¶
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 is_safe_attribute() is overridden.
>>> from jinja2.sandbox import is_internal_attribute >>> is_internal_attribute(str, “mro”) True >>> is_internal_attribute(str, “upper”) False
Parameters
obj (Any) –
attr (str) –
Return type
bool
jinja2.sandbox.modifies_known_mutable(obj, attr)¶
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
Parameters
obj (Any) –
attr (str) –
Return type
bool
Note
The Jinja sandbox alone is no solution for perfect security. Especially for web applications you have to keep in mind that users may create templates with arbitrary HTML in so it’s crucial to ensure that (if you are running multiple users on the same server) they can’t harm each other via JavaScript insertions and much more.
Also the sandbox is only as good as the configuration. We strongly recommend only passing non-shared resources to the template and use some sort of whitelisting for attributes.
Also keep in mind that templates may raise runtime or compile time errors, so make sure to catch them.
Operator Intercepting¶Changelog
New in version 2.6.
For maximum performance Jinja will let operators call directly the type specific callback methods. This means that it’s not possible to have this intercepted by overriding Environment.call(). Furthermore a conversion from operator to special method is not always directly possible due to how operators work. For instance for divisions more than one special method exist.
With Jinja 2.6 there is now support for explicit operator intercepting. This can be used to customize specific operators as necessary. In order to intercept an operator one has to override the SandboxedEnvironment.intercepted_binops attribute. Once the operator that needs to be intercepted is added to that set Jinja will generate bytecode that calls the SandboxedEnvironment.call_binop() function. For unary operators the unary attributes and methods have to be used instead.
The default implementation of SandboxedEnvironment.call_binop will use the SandboxedEnvironment.binop_table to translate operator symbols into callbacks performing the default operator behavior.
This example shows how the power (**) operator can be disabled in Jinja:
from jinja2.sandbox import SandboxedEnvironment
class MyEnvironment(SandboxedEnvironment): intercepted_binops = frozenset([‘**’])
def call\_binop(self, context, operator, left, right):
if operator \== '\*\*':
return self.undefined('the power operator is unavailable')
return SandboxedEnvironment.call\_binop(self, context,
operator, left, right)
Make sure to always call into the super method, even if you are not intercepting the call. Jinja might internally call the method to evaluate expressions.
Related news
### Impact _What kind of vulnerability is it? Who is impacted?_ All users of Nautobot versions earlier than 1.5.7 are impacted. In Nautobot 1.5.7 we have enabled sandboxed environments for the Jinja2 template engine used internally for template rendering for the following objects: - `extras.ComputedField` - `extras.CustomLink` - `extras.ExportTemplate` - `extras.Secret` - `extras.Webhook` While we are not aware of any active exploits, we have made this change as a preventative measure to protect against any potential remote code execution attacks utilizing maliciously crafted template code. This change forces the Jinja2 template engine to use a [`SandboxedEnvironment`](https://jinja.palletsprojects.com/en/3.0.x/sandbox/#sandbox) on all new installations of Nautobot. This addresses any potential unsafe code execution everywhere the helper function `nautobot.utilities.utils.render_jinja2` is called. Additionally, our documentation that was previously suggesting the direct use of `...