diff --git a/authentik/lib/default.yml b/authentik/lib/default.yml index afb775c44..daae5e9a0 100644 --- a/authentik/lib/default.yml +++ b/authentik/lib/default.yml @@ -95,6 +95,9 @@ outposts: discover: true disable_embedded_outpost: false +expressions: + restricted: false + ldap: task_timeout_hours: 2 page_size: 50 diff --git a/authentik/lib/expression/evaluator.py b/authentik/lib/expression/evaluator.py index 22c6532da..46ba5f416 100644 --- a/authentik/lib/expression/evaluator.py +++ b/authentik/lib/expression/evaluator.py @@ -6,15 +6,18 @@ from textwrap import indent from typing import Any, Iterable, Optional from cachetools import TLRUCache, cached +from django.apps import apps from django.core.exceptions import FieldError from guardian.shortcuts import get_anonymous_user from rest_framework.serializers import ValidationError +from RestrictedPython import compile_restricted, limited_builtins, safe_builtins, utility_builtins from sentry_sdk.hub import Hub from sentry_sdk.tracing import Span from structlog.stdlib import get_logger from authentik.core.models import User from authentik.events.models import Event +from authentik.lib.config import CONFIG from authentik.lib.utils.http import get_http_session from authentik.policies.models import Policy, PolicyBinding from authentik.policies.process import PolicyProcess @@ -55,6 +58,10 @@ class BaseEvaluator: "resolve_dns": BaseEvaluator.expr_resolve_dns, "reverse_dns": BaseEvaluator.expr_reverse_dns, } + for app in apps.get_app_configs(): + # Load models from each app + for model in app.get_models(): + self._globals[model.__name__] = model self._context = {} @cached(cache=TLRUCache(maxsize=32, ttu=lambda key, value, now: now + 180)) @@ -180,6 +187,18 @@ class BaseEvaluator: full_expression += f"\nresult = handler({handler_signature})" return full_expression + def compile(self, expression: str) -> Any: + """Parse expression. Raises SyntaxError or ValueError if the syntax is incorrect.""" + param_keys = self._context.keys() + compiler = ( + compile_restricted if CONFIG.get_bool("epxressions.restricted", False) else compile + ) + return compiler( + self.wrap_expression(expression, param_keys), + self._filename, + "exec", + ) + def evaluate(self, expression_source: str) -> Any: """Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised. If any exception is raised during execution, it is raised. @@ -188,17 +207,18 @@ class BaseEvaluator: span: Span span.description = self._filename span.set_data("expression", expression_source) - param_keys = self._context.keys() try: - ast_obj = compile( - self.wrap_expression(expression_source, param_keys), - self._filename, - "exec", - ) + ast_obj = self.compile(expression_source) except (SyntaxError, ValueError) as exc: self.handle_error(exc, expression_source) raise exc try: + if CONFIG.get_bool("expressions.restricted", False): + self._globals["__builtins__"] = { + **safe_builtins, + **limited_builtins, + **utility_builtins, + } _locals = self._context # Yes this is an exec, yes it is potentially bad. Since we limit what variables are # available here, and these policies can only be edited by admins, this is a risk @@ -221,13 +241,8 @@ class BaseEvaluator: def validate(self, expression: str) -> bool: """Validate expression's syntax, raise ValidationError if Syntax is invalid""" - param_keys = self._context.keys() try: - compile( - self.wrap_expression(expression, param_keys), - self._filename, - "exec", - ) + self.compile(expression) return True except (ValueError, SyntaxError) as exc: raise ValidationError(f"Expression Syntax Error: {str(exc)}") from exc diff --git a/poetry.lock b/poetry.lock index 500e2e832..f8913b4cb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand. [[package]] name = "aiohttp" @@ -3279,6 +3279,21 @@ requests = ">=2.0.0" [package.extras] rsa = ["oauthlib[signedtoken] (>=3.0.0)"] +[[package]] +name = "restrictedpython" +version = "7.0" +description = "RestrictedPython is a defined subset of the Python language which allows to provide a program input into a trusted environment." +optional = false +python-versions = ">=3.7, <3.13" +files = [ + {file = "RestrictedPython-7.0-py3-none-any.whl", hash = "sha256:8bb40a822090bed9c7b814d69345b0796db70cc86715d141efc937862f37c6d2"}, + {file = "RestrictedPython-7.0.tar.gz", hash = "sha256:53704afbbc350fdc8fb245441367be671c9f8380869201b2e8452e74fce3db14"}, +] + +[package.extras] +docs = ["Sphinx", "sphinx-rtd-theme"] +test = ["pytest", "pytest-mock"] + [[package]] name = "rich" version = "13.7.0" @@ -4437,4 +4452,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "~3.12" -content-hash = "d0fe6ae1be389f8a5ca5112aa90555e2ce0a4f336f07a1da9c43dd521e9d9340" +content-hash = "0782627c112f4cefa27fa066eb1e4c9b01882b416690f4e1348f4e61dfe02190" diff --git a/pyproject.toml b/pyproject.toml index aa5851881..3e1ee15bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -157,6 +157,7 @@ pyjwt = "*" python = "~3.12" pyyaml = "*" requests-oauthlib = "*" +restrictedpython = "*" sentry-sdk = "*" service_identity = "*" structlog = "*"