├── docs ├── requirements.txt ├── changes.rst ├── logo.jpg ├── usage │ ├── index.rst │ ├── framework_usage.rst │ ├── basic_usage.rst │ ├── api.rst │ └── policy.rst ├── contributing │ ├── changes_from38to39.rst │ ├── changes_from39to310.rst │ ├── changes_from310to311.rst │ ├── changes_from311to312.rst │ ├── changes_from312to313.rst │ ├── changes_from313to314.rst │ └── ast │ │ ├── python3_9.ast │ │ ├── python3_8.ast │ │ ├── python3_10.ast │ │ ├── python3_11.ast │ │ ├── python3_12.ast │ │ └── python3_13.ast ├── install │ └── index.rst ├── roadmap │ └── index.rst ├── index.rst └── idea.rst ├── COPYRIGHT.txt ├── tests ├── __init__.py ├── transformer │ ├── test_assert.py │ ├── operators │ │ ├── test_identity_operators.py │ │ ├── test_unary_operators.py │ │ ├── test_logical_operators.py │ │ ├── test_bool_operators.py │ │ ├── test_comparison_operators.py │ │ ├── test_bit_wise_operators.py │ │ └── test_arithmetic_operators.py │ ├── test_breakpoint.py │ ├── test_generic.py │ ├── test_base_types.py │ ├── test_eval_exec.py │ ├── test_slice.py │ ├── test_global_local.py │ ├── test_loop.py │ ├── test_dict_comprehension.py │ ├── test_conditional.py │ ├── test_assign.py │ ├── test_fstring.py │ ├── test_augassign.py │ ├── test_inspect.py │ ├── test_yield.py │ ├── test_lambda.py │ ├── test_functiondef.py │ ├── test_comparators.py │ ├── test_async.py │ ├── test_tstring.py │ ├── test_import.py │ ├── test_call.py │ ├── test_attribute.py │ ├── test_iterator.py │ ├── test_try.py │ ├── test_with_stmt.py │ ├── test_subscript.py │ ├── test_classdef.py │ └── test_name.py ├── test_iterating_over_dict_items.py ├── helper.py ├── test_Utilities.py ├── test_imports.py ├── test_NamedExpr.py ├── builtins │ ├── test_limits.py │ └── test_utilities.py ├── test_eval.py └── test_compile_restricted_function.py ├── constraints.txt ├── .gitignore ├── src └── RestrictedPython │ ├── _compat.py │ ├── PrintCollector.py │ ├── __init__.py │ ├── Limits.py │ ├── Utilities.py │ ├── Eval.py │ └── compile.py ├── MANIFEST.in ├── .readthedocs.yaml ├── pyproject.toml ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── setup.cfg ├── .github └── workflows │ ├── pre-commit.yml │ └── tests.yml ├── .editorconfig ├── LICENSE.txt ├── .meta.toml ├── setup.py ├── tox.ini └── README.rst /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx 2 | furo 3 | -------------------------------------------------------------------------------- /COPYRIGHT.txt: -------------------------------------------------------------------------------- 1 | Zope Foundation and Contributors -------------------------------------------------------------------------------- /docs/changes.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../CHANGES.rst 2 | -------------------------------------------------------------------------------- /docs/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zopefoundation/RestrictedPython/HEAD/docs/logo.jpg -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # Make it a package so we can import from helper.py inside the tests. 2 | -------------------------------------------------------------------------------- /docs/usage/index.rst: -------------------------------------------------------------------------------- 1 | Usage of RestrictedPython 2 | ========================= 3 | 4 | .. toctree:: 5 | :maxdepth: 1 6 | 7 | basic_usage 8 | framework_usage 9 | policy 10 | -------------------------------------------------------------------------------- /constraints.txt: -------------------------------------------------------------------------------- 1 | # Constraints for Python packages 2 | # ------------------------------- 3 | # Pin versions / version ranges if necessary. 4 | isort >= 4.3.2 5 | pytest >= 5 6 | coverage >= 6 7 | -------------------------------------------------------------------------------- /docs/contributing/changes_from38to39.rst: -------------------------------------------------------------------------------- 1 | Changes from Python 3.8 to Python 3.9 2 | ------------------------------------- 3 | 4 | .. literalinclude:: ast/python3_9.ast 5 | :diff: ast/python3_8.ast 6 | -------------------------------------------------------------------------------- /docs/contributing/changes_from39to310.rst: -------------------------------------------------------------------------------- 1 | Changes from Python 3.9 to Python 3.10 2 | -------------------------------------- 3 | 4 | .. literalinclude:: ast/python3_10.ast 5 | :diff: ast/python3_9.ast 6 | -------------------------------------------------------------------------------- /docs/contributing/changes_from310to311.rst: -------------------------------------------------------------------------------- 1 | Changes from Python 3.10 to Python 3.11 2 | --------------------------------------- 3 | 4 | .. literalinclude:: ast/python3_11.ast 5 | :diff: ast/python3_10.ast 6 | -------------------------------------------------------------------------------- /docs/contributing/changes_from311to312.rst: -------------------------------------------------------------------------------- 1 | Changes from Python 3.11 to Python 3.12 2 | --------------------------------------- 3 | 4 | .. literalinclude:: ast/python3_12.ast 5 | :diff: ast/python3_11.ast 6 | -------------------------------------------------------------------------------- /docs/contributing/changes_from312to313.rst: -------------------------------------------------------------------------------- 1 | Changes from Python 3.12 to Python 3.13 2 | --------------------------------------- 3 | 4 | .. literalinclude:: ast/python3_13.ast 5 | :diff: ast/python3_12.ast 6 | -------------------------------------------------------------------------------- /docs/contributing/changes_from313to314.rst: -------------------------------------------------------------------------------- 1 | Changes from Python 3.13 to Python 3.14 2 | --------------------------------------- 3 | 4 | .. literalinclude:: ast/python3_14.ast 5 | :diff: ast/python3_13.ast 6 | -------------------------------------------------------------------------------- /tests/transformer/test_assert.py: -------------------------------------------------------------------------------- 1 | from tests.helper import restricted_exec 2 | 3 | 4 | def test_RestrictingNodeTransformer__visit_Assert__1(): 5 | """It allows assert statements.""" 6 | restricted_exec('assert 1') 7 | -------------------------------------------------------------------------------- /tests/transformer/operators/test_identity_operators.py: -------------------------------------------------------------------------------- 1 | from tests.helper import restricted_eval 2 | 3 | 4 | def test_Is(): 5 | assert restricted_eval('True is True') is True 6 | 7 | 8 | def test_NotIs(): 9 | assert restricted_eval('1 is not True') is True 10 | -------------------------------------------------------------------------------- /tests/transformer/operators/test_unary_operators.py: -------------------------------------------------------------------------------- 1 | from tests.helper import restricted_eval 2 | 3 | 4 | def test_UAdd(): 5 | assert restricted_eval('+a', {'a': 42}) == 42 6 | 7 | 8 | def test_USub(): 9 | assert restricted_eval('-a', {'a': 2411}) == -2411 10 | -------------------------------------------------------------------------------- /tests/transformer/operators/test_logical_operators.py: -------------------------------------------------------------------------------- 1 | from tests.helper import restricted_eval 2 | 3 | 4 | def test_In(): 5 | assert restricted_eval('1 in [1, 2, 3]') is True 6 | 7 | 8 | def test_NotIn(): 9 | assert restricted_eval('4 not in [1, 2, 3]') is True 10 | -------------------------------------------------------------------------------- /tests/transformer/operators/test_bool_operators.py: -------------------------------------------------------------------------------- 1 | from tests.helper import restricted_eval 2 | 3 | 4 | def test_Or(): 5 | assert restricted_eval('False or True') is True 6 | 7 | 8 | def test_And(): 9 | assert restricted_eval('True and True') is True 10 | 11 | 12 | def test_Not(): 13 | assert restricted_eval('not False') is True 14 | -------------------------------------------------------------------------------- /docs/install/index.rst: -------------------------------------------------------------------------------- 1 | Install / Depend on RestrictedPython 2 | ==================================== 3 | 4 | RestrictedPython is usually not used stand alone, if you use it in context of your package add it to ``install_requires`` in your ``setup.py`` or a ``requirement.txt`` used by ``pip``. 5 | 6 | For a standalone usage: 7 | 8 | .. code-block:: console 9 | 10 | $ pip install RestrictedPython 11 | -------------------------------------------------------------------------------- /tests/transformer/test_breakpoint.py: -------------------------------------------------------------------------------- 1 | from RestrictedPython import compile_restricted_exec 2 | 3 | 4 | def test_call_breakpoint(): 5 | """The Python3.7+ builtin function breakpoint should not 6 | be used and is forbidden in RestrictedPython. 7 | """ 8 | result = compile_restricted_exec('breakpoint()') 9 | assert result.errors == ('Line 1: "breakpoint" is a reserved name.',) 10 | assert result.code is None 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | *.dll 4 | *.egg-info/ 5 | *.profraw 6 | *.pyc 7 | *.pyo 8 | *.so 9 | .coverage 10 | .coverage.* 11 | .eggs/ 12 | .installed.cfg 13 | .mr.developer.cfg 14 | .tox/ 15 | .vscode/ 16 | __pycache__/ 17 | bin/ 18 | build/ 19 | coverage.xml 20 | develop-eggs/ 21 | develop/ 22 | dist/ 23 | docs/_build 24 | eggs/ 25 | etc/ 26 | lib/ 27 | lib64 28 | log/ 29 | parts/ 30 | pyvenv.cfg 31 | testing.log 32 | var/ 33 | -------------------------------------------------------------------------------- /src/RestrictedPython/_compat.py: -------------------------------------------------------------------------------- 1 | import platform 2 | import sys 3 | 4 | 5 | _version = sys.version_info 6 | IS_PY310_OR_GREATER = _version.major == 3 and _version.minor >= 10 7 | IS_PY311_OR_GREATER = _version.major == 3 and _version.minor >= 11 8 | IS_PY312_OR_GREATER = _version.major == 3 and _version.minor >= 12 9 | IS_PY313_OR_GREATER = _version.major == 3 and _version.minor >= 13 10 | IS_PY314_OR_GREATER = _version.major == 3 and _version.minor >= 14 11 | 12 | IS_CPYTHON = platform.python_implementation() == 'CPython' 13 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | include *.md 4 | include *.rst 5 | include *.txt 6 | include buildout.cfg 7 | include tox.ini 8 | include .pre-commit-config.yaml 9 | 10 | recursive-include docs *.py 11 | recursive-include docs *.rst 12 | recursive-include docs *.txt 13 | recursive-include docs Makefile 14 | 15 | recursive-include src *.py 16 | include *.yaml 17 | recursive-include docs *.ast 18 | recursive-include docs *.bat 19 | recursive-include docs *.jpg 20 | recursive-include tests *.py 21 | -------------------------------------------------------------------------------- /tests/transformer/operators/test_comparison_operators.py: -------------------------------------------------------------------------------- 1 | from tests.helper import restricted_eval 2 | 3 | 4 | def test_Eq(): 5 | assert restricted_eval('1 == 1') is True 6 | 7 | 8 | def test_NotEq(): 9 | assert restricted_eval('1 != 2') is True 10 | 11 | 12 | def test_Gt(): 13 | assert restricted_eval('2 > 1') is True 14 | 15 | 16 | def test_Lt(): 17 | assert restricted_eval('1 < 2') 18 | 19 | 20 | def test_GtE(): 21 | assert restricted_eval('2 >= 2') is True 22 | 23 | 24 | def test_LtE(): 25 | assert restricted_eval('1 <= 2') is True 26 | -------------------------------------------------------------------------------- /tests/transformer/operators/test_bit_wise_operators.py: -------------------------------------------------------------------------------- 1 | from tests.helper import restricted_eval 2 | 3 | 4 | def test_BitAnd(): 5 | assert restricted_eval('5 & 3') == 1 6 | 7 | 8 | def test_BitOr(): 9 | assert restricted_eval('5 | 3') == 7 10 | 11 | 12 | def test_BitXor(): 13 | assert restricted_eval('5 ^ 3') == 6 14 | 15 | 16 | def test_Invert(): 17 | assert restricted_eval('~17') == -18 18 | 19 | 20 | def test_LShift(): 21 | assert restricted_eval('8 << 2') == 32 22 | 23 | 24 | def test_RShift(): 25 | assert restricted_eval('8 >> 1') == 4 26 | -------------------------------------------------------------------------------- /tests/transformer/test_generic.py: -------------------------------------------------------------------------------- 1 | import ast 2 | 3 | from RestrictedPython import RestrictingNodeTransformer 4 | 5 | 6 | def test_RestrictingNodeTransformer__generic_visit__1(): 7 | """It log an error if there is an unknown ast node visited.""" 8 | class MyFancyNode(ast.AST): 9 | pass 10 | 11 | transformer = RestrictingNodeTransformer() 12 | transformer.visit(MyFancyNode()) 13 | assert transformer.errors == [ 14 | 'Line None: MyFancyNode statements are not allowed.'] 15 | assert transformer.warnings == [ 16 | 'Line None: MyFancyNode statement is not known to RestrictedPython'] 17 | -------------------------------------------------------------------------------- /tests/transformer/test_base_types.py: -------------------------------------------------------------------------------- 1 | from RestrictedPython import compile_restricted_exec 2 | from tests.helper import restricted_eval 3 | 4 | 5 | def test_Num(): 6 | """It allows to use number literals.""" 7 | assert restricted_eval('42') == 42 8 | 9 | 10 | def test_Bytes(): 11 | """It allows to use bytes literals.""" 12 | assert restricted_eval('b"code"') == b"code" 13 | 14 | 15 | def test_Set(): 16 | """It allows to use set literals.""" 17 | assert restricted_eval('{1, 2, 3}') == {1, 2, 3} 18 | 19 | 20 | def test_Ellipsis(): 21 | """It prevents using the `ellipsis` statement.""" 22 | result = compile_restricted_exec('...') 23 | assert result.errors == ('Line 1: Ellipsis statements are not allowed.',) 24 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | # Read the Docs configuration file 4 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 5 | 6 | # Required 7 | version: 2 8 | 9 | # Set the version of Python and other tools you might need 10 | build: 11 | os: ubuntu-22.04 12 | tools: 13 | python: "3.11" 14 | 15 | # Build documentation in the docs/ directory with Sphinx 16 | sphinx: 17 | configuration: docs/conf.py 18 | 19 | # We recommend specifying your dependencies to enable reproducible builds: 20 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 21 | python: 22 | install: 23 | - requirements: docs/requirements.txt 24 | - method: pip 25 | path: . 26 | -------------------------------------------------------------------------------- /tests/transformer/test_eval_exec.py: -------------------------------------------------------------------------------- 1 | from RestrictedPython import compile_restricted_exec 2 | 3 | 4 | EXEC_FUNCTION = """\ 5 | def no_exec(): 6 | exec('q = 1') 7 | """ 8 | 9 | 10 | def test_RestrictingNodeTransformer__visit_Exec__2(): 11 | """It is an error if the code call the `exec` function.""" 12 | result = compile_restricted_exec(EXEC_FUNCTION) 13 | assert result.errors == ("Line 2: Exec calls are not allowed.",) 14 | 15 | 16 | EVAL_FUNCTION = """\ 17 | def no_eval(): 18 | eval('q = 1') 19 | """ 20 | 21 | 22 | def test_RestrictingNodeTransformer__visit_Eval__1(): 23 | """It is an error if the code call the `eval` function.""" 24 | result = compile_restricted_exec(EVAL_FUNCTION) 25 | assert result.errors == ("Line 2: Eval calls are not allowed.",) 26 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # 2 | # Generated from: 3 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 4 | 5 | [build-system] 6 | requires = [ 7 | "setuptools >= 78.1.1,< 81", 8 | "wheel", 9 | ] 10 | build-backend = "setuptools.build_meta" 11 | 12 | [tool.coverage.run] 13 | branch = true 14 | source = ["RestrictedPython"] 15 | 16 | [tool.coverage.report] 17 | fail_under = 97.2 18 | precision = 2 19 | ignore_errors = true 20 | show_missing = true 21 | exclude_lines = [ 22 | "pragma: no cover", 23 | "pragma: nocover", 24 | "except ImportError:", 25 | "raise NotImplementedError", 26 | "if __name__ == '__main__':", 27 | "self.fail", 28 | "raise AssertionError", 29 | "raise unittest.Skip", 30 | ] 31 | 32 | [tool.coverage.html] 33 | directory = "parts/htmlcov" 34 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | minimum_pre_commit_version: '3.6' 4 | repos: 5 | - repo: https://github.com/pycqa/isort 6 | rev: "7.0.0" 7 | hooks: 8 | - id: isort 9 | - repo: https://github.com/hhatto/autopep8 10 | rev: "v2.3.2" 11 | hooks: 12 | - id: autopep8 13 | args: [--in-place, --aggressive, --aggressive] 14 | - repo: https://github.com/asottile/pyupgrade 15 | rev: v3.21.0 16 | hooks: 17 | - id: pyupgrade 18 | args: [--py39-plus] 19 | - repo: https://github.com/isidentical/teyit 20 | rev: 0.4.3 21 | hooks: 22 | - id: teyit 23 | - repo: https://github.com/PyCQA/flake8 24 | rev: "7.3.0" 25 | hooks: 26 | - id: flake8 27 | additional_dependencies: 28 | - flake8-debugger == 4.1.2 29 | -------------------------------------------------------------------------------- /tests/transformer/test_slice.py: -------------------------------------------------------------------------------- 1 | from operator import getitem 2 | 3 | from tests.helper import restricted_eval 4 | 5 | 6 | def test_slice(): 7 | rglb = {'_getitem_': getitem} # restricted globals 8 | 9 | assert restricted_eval('[1, 2, 3, 4, 5]', rglb) == [1, 2, 3, 4, 5] 10 | assert restricted_eval('[1, 2, 3, 4, 5][:]', rglb) == [1, 2, 3, 4, 5] 11 | assert restricted_eval('[1, 2, 3, 4, 5][1:]', rglb) == [2, 3, 4, 5] 12 | assert restricted_eval('[1, 2, 3, 4, 5][:4]', rglb) == [1, 2, 3, 4] 13 | assert restricted_eval('[1, 2, 3, 4, 5][1:4]', rglb) == [2, 3, 4] 14 | assert restricted_eval('[1, 2, 3, 4, 5][::3]', rglb) == [1, 4] 15 | assert restricted_eval('[1, 2, 3, 4, 5][1::3]', rglb) == [2, 5] 16 | assert restricted_eval('[1, 2, 3, 4, 5][:4:3]', rglb) == [1, 4] 17 | assert restricted_eval('[1, 2, 3, 4, 5][1:4:3]', rglb) == [2] 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 5 | # Contributing to zopefoundation projects 6 | 7 | The projects under the zopefoundation GitHub organization are open source and 8 | welcome contributions in different forms: 9 | 10 | * bug reports 11 | * code improvements and bug fixes 12 | * documentation improvements 13 | * pull request reviews 14 | 15 | For any changes in the repository besides trivial typo fixes you are required 16 | to sign the contributor agreement. See 17 | https://www.zope.dev/developer/becoming-a-committer.html for details. 18 | 19 | Please visit our [Developer 20 | Guidelines](https://www.zope.dev/developer/guidelines.html) if you'd like to 21 | contribute code changes and our [guidelines for reporting 22 | bugs](https://www.zope.dev/developer/reporting-bugs.html) if you want to file a 23 | bug report. 24 | -------------------------------------------------------------------------------- /tests/transformer/test_global_local.py: -------------------------------------------------------------------------------- 1 | from RestrictedPython import compile_restricted_exec 2 | from tests.helper import restricted_exec 3 | 4 | 5 | GLOBAL_EXAMPLE = """ 6 | def x(): 7 | global a 8 | a = 11 9 | x() 10 | """ 11 | 12 | 13 | def test_Global(): 14 | glb = {'a': None} 15 | restricted_exec(GLOBAL_EXAMPLE, glb) 16 | assert glb['a'] == 11 17 | 18 | 19 | # Example from: 20 | # https://www.smallsurething.com/a-quick-guide-to-nonlocal-in-python-3/ 21 | NONLOCAL_EXAMPLE = """ 22 | def outside(): 23 | msg = "Outside!" 24 | def inside(): 25 | nonlocal msg 26 | msg = "Inside!" 27 | print(msg) 28 | inside() 29 | print(msg) 30 | outside() 31 | """ 32 | 33 | 34 | def test_Nonlocal(): 35 | result = compile_restricted_exec(NONLOCAL_EXAMPLE) 36 | assert result.errors == ('Line 5: Nonlocal statements are not allowed.',) 37 | assert result.code is None 38 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | 4 | [flake8] 5 | doctests = 1 6 | 7 | [check-manifest] 8 | ignore = 9 | .editorconfig 10 | .meta.toml 11 | docs/_build/html/_sources/* 12 | docs/_build/doctest/* 13 | docs/CHANGES.rst 14 | docs/_build/html/_images/* 15 | docs/_build/html/_sources/contributing/* 16 | docs/_build/html/_sources/install/* 17 | docs/_build/html/_sources/roadmap/* 18 | docs/_build/html/_sources/upgrade_dependencies/* 19 | docs/_build/html/_sources/usage/* 20 | docs/_build/html/_static/scripts/* 21 | 22 | [isort] 23 | force_single_line = True 24 | combine_as_imports = True 25 | sections = FUTURE,STDLIB,THIRDPARTY,ZOPE,FIRSTPARTY,LOCALFOLDER 26 | known_third_party = docutils, pkg_resources, pytz 27 | known_zope = 28 | known_first_party = 29 | default_section = ZOPE 30 | line_length = 79 31 | lines_after_imports = 2 32 | -------------------------------------------------------------------------------- /tests/transformer/test_loop.py: -------------------------------------------------------------------------------- 1 | from tests.helper import restricted_exec 2 | 3 | 4 | WHILE = """\ 5 | a = 5 6 | while a < 7: 7 | a = a + 3 8 | """ 9 | 10 | 11 | def test_RestrictingNodeTransformer__visit_While__1(): 12 | """It allows `while` statements.""" 13 | glb = restricted_exec(WHILE) 14 | assert glb['a'] == 8 15 | 16 | 17 | BREAK = """\ 18 | a = 5 19 | while True: 20 | a = a + 3 21 | if a >= 7: 22 | break 23 | """ 24 | 25 | 26 | def test_RestrictingNodeTransformer__visit_Break__1(): 27 | """It allows `break` statements.""" 28 | glb = restricted_exec(BREAK) 29 | assert glb['a'] == 8 30 | 31 | 32 | CONTINUE = """\ 33 | a = 3 34 | while a < 10: 35 | if a < 5: 36 | a = a + 1 37 | continue 38 | a = a + 10 39 | """ 40 | 41 | 42 | def test_RestrictingNodeTransformer__visit_Continue__1(): 43 | """It allows `continue` statements.""" 44 | glb = restricted_exec(CONTINUE) 45 | assert glb['a'] == 15 46 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | name: pre-commit 4 | 5 | on: 6 | pull_request: 7 | push: 8 | branches: 9 | - master 10 | # Allow to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | env: 14 | FORCE_COLOR: 1 15 | 16 | jobs: 17 | pre-commit: 18 | permissions: 19 | contents: read 20 | pull-requests: write 21 | name: linting 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v5 25 | - uses: actions/setup-python@v6 26 | with: 27 | python-version: 3.x 28 | - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd #v3.0.1 29 | with: 30 | extra_args: --all-files --show-diff-on-failure 31 | env: 32 | PRE_COMMIT_COLOR: always 33 | - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 #v1.1.0 34 | if: always() 35 | with: 36 | msg: Apply pre-commit code formatting 37 | -------------------------------------------------------------------------------- /tests/transformer/operators/test_arithmetic_operators.py: -------------------------------------------------------------------------------- 1 | from tests.helper import restricted_eval 2 | 3 | 4 | # Arithmetic Operators 5 | 6 | def test_Add(): 7 | assert restricted_eval('1 + 1') == 2 8 | 9 | 10 | def test_Sub(): 11 | assert restricted_eval('5 - 3') == 2 12 | 13 | 14 | def test_Mult(): 15 | assert restricted_eval('2 * 2') == 4 16 | 17 | 18 | def test_Div(): 19 | assert restricted_eval('10 / 2') == 5 20 | 21 | 22 | def test_Mod(): 23 | assert restricted_eval('10 % 3') == 1 24 | 25 | 26 | def test_Pow(): 27 | assert restricted_eval('2 ** 8') == 256 28 | 29 | 30 | def test_FloorDiv(): 31 | assert restricted_eval('7 // 2') == 3 32 | 33 | 34 | def test_MatMult(): 35 | class Vector: 36 | def __init__(self, values): 37 | self.values = values 38 | 39 | def __matmul__(self, other): 40 | return sum(x * y for x, y in zip(self.values, other.values)) 41 | 42 | assert restricted_eval( 43 | 'Vector((8, 3, 5)) @ Vector((2, 7, 1))', {'Vector': Vector}) == 42 44 | -------------------------------------------------------------------------------- /tests/transformer/test_dict_comprehension.py: -------------------------------------------------------------------------------- 1 | from tests.helper import restricted_exec 2 | 3 | 4 | DICT_COMPREHENSION_WITH_ATTRS = """ 5 | def call(seq): 6 | return {y.k: y.v for y in seq.z if y.k} 7 | """ 8 | 9 | 10 | def test_dict_comprehension_with_attrs(mocker): 11 | _getattr_ = mocker.Mock() 12 | _getattr_.side_effect = getattr 13 | 14 | _getiter_ = mocker.Mock() 15 | _getiter_.side_effect = lambda ob: ob 16 | 17 | glb = {'_getattr_': _getattr_, '_getiter_': _getiter_} 18 | restricted_exec(DICT_COMPREHENSION_WITH_ATTRS, glb) 19 | 20 | z = [mocker.Mock(k=0, v='a'), mocker.Mock(k=1, v='b')] 21 | seq = mocker.Mock(z=z) 22 | 23 | ret = glb['call'](seq) 24 | assert ret == {1: 'b'} 25 | 26 | _getiter_.assert_called_once_with(z) 27 | 28 | calls = [mocker.call(seq, 'z')] 29 | 30 | calls.extend([ 31 | mocker.call(z[0], 'k'), 32 | mocker.call(z[1], 'k'), 33 | mocker.call(z[1], 'k'), 34 | mocker.call(z[1], 'v'), 35 | ]) 36 | 37 | _getattr_.assert_has_calls(calls) 38 | -------------------------------------------------------------------------------- /tests/transformer/test_conditional.py: -------------------------------------------------------------------------------- 1 | from tests.helper import restricted_exec 2 | 3 | 4 | def test_RestrictingNodeTransformer__test_ternary_if( 5 | mocker): 6 | src = 'x.y = y.a if y.z else y.b' 7 | _getattr_ = mocker.stub() 8 | _getattr_.side_effect = lambda ob, key: ob[key] 9 | _write_ = mocker.stub() 10 | _write_.side_effect = lambda ob: ob 11 | 12 | glb = { 13 | '_getattr_': _getattr_, 14 | '_write_': _write_, 15 | 'x': mocker.stub(), 16 | 'y': {'a': 'a', 'b': 'b'}, 17 | } 18 | 19 | glb['y']['z'] = True 20 | restricted_exec(src, glb) 21 | 22 | assert glb['x'].y == 'a' 23 | _write_.assert_called_once_with(glb['x']) 24 | _getattr_.assert_has_calls([ 25 | mocker.call(glb['y'], 'z'), 26 | mocker.call(glb['y'], 'a')]) 27 | 28 | _write_.reset_mock() 29 | _getattr_.reset_mock() 30 | 31 | glb['y']['z'] = False 32 | restricted_exec(src, glb) 33 | 34 | assert glb['x'].y == 'b' 35 | _write_.assert_called_once_with(glb['x']) 36 | _getattr_.assert_has_calls([ 37 | mocker.call(glb['y'], 'z'), 38 | mocker.call(glb['y'], 'b')]) 39 | -------------------------------------------------------------------------------- /tests/test_iterating_over_dict_items.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from RestrictedPython import compile_restricted_exec 4 | from RestrictedPython import safe_globals 5 | from RestrictedPython.Eval import default_guarded_getiter 6 | from RestrictedPython.Guards import guarded_iter_unpack_sequence 7 | 8 | 9 | ITERATE_OVER_DICT_ITEMS = """ 10 | d = {'a': 'b'} 11 | for k, v in d.items(): 12 | pass 13 | """ 14 | 15 | 16 | def test_iterate_over_dict_items_plain(): 17 | glb = {} 18 | result = compile_restricted_exec(ITERATE_OVER_DICT_ITEMS) 19 | assert result.code is not None 20 | assert result.errors == () 21 | with pytest.raises(NameError) as excinfo: 22 | exec(result.code, glb, None) 23 | assert "name '_iter_unpack_sequence_' is not defined" in str(excinfo.value) 24 | 25 | 26 | def test_iterate_over_dict_items_safe(): 27 | glb = safe_globals.copy() 28 | glb['_getiter_'] = default_guarded_getiter 29 | glb['_iter_unpack_sequence_'] = guarded_iter_unpack_sequence 30 | result = compile_restricted_exec(ITERATE_OVER_DICT_ITEMS) 31 | assert result.code is not None 32 | assert result.errors == () 33 | exec(result.code, glb, None) 34 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | # 4 | # EditorConfig Configuration file, for more details see: 5 | # http://EditorConfig.org 6 | # EditorConfig is a convention description, that could be interpreted 7 | # by multiple editors to enforce common coding conventions for specific 8 | # file types 9 | 10 | # top-most EditorConfig file: 11 | # Will ignore other EditorConfig files in Home directory or upper tree level. 12 | root = true 13 | 14 | 15 | [*] # For All Files 16 | # Unix-style newlines with a newline ending every file 17 | end_of_line = lf 18 | insert_final_newline = true 19 | trim_trailing_whitespace = true 20 | # Set default charset 21 | charset = utf-8 22 | # Indent style default 23 | indent_style = space 24 | # Max Line Length - a hard line wrap, should be disabled 25 | max_line_length = off 26 | 27 | [*.{py,cfg,ini}] 28 | # 4 space indentation 29 | indent_size = 4 30 | 31 | [*.{yml,zpt,pt,dtml,zcml}] 32 | # 2 space indentation 33 | indent_size = 2 34 | 35 | [{Makefile,.gitmodules}] 36 | # Tab indentation (no size specified, but view as 4 spaces) 37 | indent_style = tab 38 | indent_size = unset 39 | tab_width = unset 40 | -------------------------------------------------------------------------------- /docs/roadmap/index.rst: -------------------------------------------------------------------------------- 1 | Roadmap 2 | ======= 3 | 4 | .. todo:: 5 | 6 | Complete documentation of all public API elements with docstyle comments 7 | https://www.python.org/dev/peps/pep-0257/ 8 | http://www.sphinx-doc.org/en/stable/ext/autodoc.html 9 | http://thomas-cokelaer.info/tutorials/sphinx/docstring_python.html 10 | 11 | RestrictedPython 4.0 (finished) 12 | ------------------------------- 13 | 14 | A feature complete rewrite of RestrictedPython using ``ast`` module instead of ``compile`` package. 15 | RestrictedPython 4.0 should not add any new or remove restrictions. 16 | 17 | A detailed documentation that support usage and further development. 18 | 19 | Full code coverage tests. 20 | 21 | .. todo:: 22 | 23 | Resolve Discussion in https://github.com/zopefoundation/RestrictedPython/pull/39#issuecomment-283074699 24 | 25 | compile_restricted optional params flags and dont_inherit will not work as expected with the current implementation. 26 | 27 | stephan-hof did propose a solution, should be discussed and if approved implemented. 28 | 29 | RestrictedPython 6.0+ 30 | --------------------- 31 | 32 | * Python 3+ only, no more support for Python 2.7 33 | * mypy - Static Code Analysis Annotations 34 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. RestrictedPython documentation master file, created by 2 | sphinx-quickstart on Thu May 19 12:43:20 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | .. image:: logo.jpg 7 | 8 | ============================================ 9 | Welcome to RestrictedPython's documentation! 10 | ============================================ 11 | 12 | RestrictedPython is a tool that helps to define a subset of the Python language which allows to provide a program input into a trusted environment. 13 | RestrictedPython is not a sandbox system or a secured environment, but it helps to define a trusted environment and execute untrusted code inside of it. 14 | 15 | Supported Python versions 16 | ========================= 17 | 18 | RestrictedPython supports CPython 3.9 up to 3.13. 19 | It does _not_ support PyPy or other alternative Python implementations. 20 | 21 | Contents 22 | ======== 23 | 24 | .. toctree:: 25 | :maxdepth: 2 26 | 27 | idea 28 | install/index 29 | usage/index 30 | usage/api 31 | 32 | roadmap/index 33 | contributing/index 34 | 35 | changes 36 | 37 | Indices and tables 38 | ================== 39 | 40 | * :ref:`genindex` 41 | * :ref:`modindex` 42 | * :ref:`search` 43 | -------------------------------------------------------------------------------- /src/RestrictedPython/PrintCollector.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2002 Zope Foundation and Contributors. 4 | # 5 | # This software is subject to the provisions of the Zope Public License, 6 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 7 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 8 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 9 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 10 | # FOR A PARTICULAR PURPOSE 11 | # 12 | ############################################################################## 13 | 14 | 15 | class PrintCollector: 16 | """Collect written text, and return it when called.""" 17 | 18 | def __init__(self, _getattr_=None): 19 | self.txt = [] 20 | self._getattr_ = _getattr_ 21 | 22 | def write(self, text): 23 | self.txt.append(text) 24 | 25 | def __call__(self): 26 | return ''.join(self.txt) 27 | 28 | def _call_print(self, *objects, **kwargs): 29 | if kwargs.get('file', None) is None: 30 | kwargs['file'] = self 31 | else: 32 | self._getattr_(kwargs['file'], 'write') 33 | 34 | print(*objects, **kwargs) 35 | -------------------------------------------------------------------------------- /tests/helper.py: -------------------------------------------------------------------------------- 1 | import RestrictedPython.Guards 2 | from RestrictedPython import compile_restricted_eval 3 | from RestrictedPython import compile_restricted_exec 4 | 5 | 6 | def _compile(compile_func, source): 7 | """Compile some source with a compile func.""" 8 | result = compile_func(source) 9 | assert result.errors == (), result.errors 10 | assert result.code is not None 11 | return result.code 12 | 13 | 14 | def _execute(code, glb, exc_func): 15 | """Execute compiled code using `exc_func`. 16 | 17 | glb ... globals, gets injected with safe_builtins 18 | """ 19 | if glb is None: 20 | glb = {} 21 | if '__builtins__' not in glb: 22 | glb['__builtins__'] = RestrictedPython.Guards.safe_builtins.copy() 23 | if exc_func == 'eval': 24 | return eval(code, glb) 25 | else: 26 | exec(code, glb) 27 | return glb 28 | 29 | 30 | def restricted_eval(source, glb=None): 31 | """Call compile_restricted_eval and actually eval it.""" 32 | code = _compile(compile_restricted_eval, source) 33 | return _execute(code, glb, 'eval') 34 | 35 | 36 | def restricted_exec(source, glb=None): 37 | """Call compile_restricted_eval and actually exec it.""" 38 | code = _compile(compile_restricted_exec, source) 39 | return _execute(code, glb, 'exec') 40 | -------------------------------------------------------------------------------- /tests/transformer/test_assign.py: -------------------------------------------------------------------------------- 1 | from RestrictedPython.Guards import guarded_unpack_sequence 2 | from tests.helper import restricted_exec 3 | 4 | 5 | def test_RestrictingNodeTransformer__visit_Assign__1(mocker): 6 | src = "orig = (a, (x, z)) = (c, d) = g" 7 | 8 | _getiter_ = mocker.stub() 9 | _getiter_.side_effect = lambda it: it 10 | 11 | glb = { 12 | '_getiter_': _getiter_, 13 | '_unpack_sequence_': guarded_unpack_sequence, 14 | 'g': (1, (2, 3)), 15 | } 16 | 17 | restricted_exec(src, glb) 18 | assert glb['a'] == 1 19 | assert glb['x'] == 2 20 | assert glb['z'] == 3 21 | assert glb['c'] == 1 22 | assert glb['d'] == (2, 3) 23 | assert glb['orig'] == (1, (2, 3)) 24 | assert _getiter_.call_count == 3 25 | _getiter_.assert_any_call((1, (2, 3))) 26 | _getiter_.assert_any_call((2, 3)) 27 | _getiter_.reset_mock() 28 | 29 | 30 | def test_RestrictingNodeTransformer__visit_Assign__2( 31 | mocker): 32 | src = "a, *d, (c, *e), x = (1, 2, 3, (4, 3, 4), 5)" 33 | 34 | _getiter_ = mocker.stub() 35 | _getiter_.side_effect = lambda it: it 36 | 37 | glb = { 38 | '_getiter_': _getiter_, 39 | '_unpack_sequence_': guarded_unpack_sequence 40 | } 41 | 42 | restricted_exec(src, glb) 43 | assert glb['a'] == 1 44 | assert glb['d'] == [2, 3] 45 | assert glb['c'] == 4 46 | assert glb['e'] == [3, 4] 47 | assert glb['x'] == 5 48 | 49 | _getiter_.assert_has_calls([ 50 | mocker.call((1, 2, 3, (4, 3, 4), 5)), 51 | mocker.call((4, 3, 4))]) 52 | -------------------------------------------------------------------------------- /tests/transformer/test_fstring.py: -------------------------------------------------------------------------------- 1 | from RestrictedPython import compile_restricted_exec 2 | from RestrictedPython.PrintCollector import PrintCollector 3 | 4 | 5 | def test_transform(): 6 | """It compiles a function call successfully and returns the used name.""" 7 | 8 | result = compile_restricted_exec('a = f"{max([1, 2, 3])}"') 9 | assert result.errors == () 10 | loc = {} 11 | exec(result.code, {}, loc) 12 | assert loc['a'] == '3' 13 | assert result.used_names == {'max': True} 14 | 15 | 16 | def test_visit_invalid_variable_name(): 17 | """Accessing private attributes is forbidden. 18 | 19 | This is just a smoke test to validate that restricted exec is used 20 | in the run-time evaluation of f-strings. 21 | """ 22 | result = compile_restricted_exec('f"{__init__}"') 23 | assert result.errors == ( 24 | 'Line 1: "__init__" is an invalid variable name because it starts with "_"', # NOQA: E501 25 | ) 26 | 27 | 28 | f_string_self_documenting_expressions_example = """ 29 | from datetime import date 30 | 31 | user = 'eric_idle' 32 | member_since = date(1975, 7, 31) 33 | print(f'{user=} {member_since=}') 34 | """ 35 | 36 | 37 | def test_f_string_self_documenting_expressions(): 38 | """Checks if f-string self-documenting expressions is checked.""" 39 | result = compile_restricted_exec( 40 | f_string_self_documenting_expressions_example, 41 | ) 42 | assert result.errors == () 43 | 44 | glb = {'_print_': PrintCollector, '_getattr_': None} 45 | exec(result.code, glb) 46 | assert glb['_print']() == "user='eric_idle' member_since=datetime.date(1975, 7, 31)\n" # NOQA: E501 47 | -------------------------------------------------------------------------------- /tests/test_Utilities.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from RestrictedPython.Utilities import reorder 4 | from RestrictedPython.Utilities import test 5 | from RestrictedPython.Utilities import utility_builtins 6 | 7 | 8 | def test_Utilities__test_1(): 9 | """It returns the first arg after the first argument which is True""" 10 | assert test(True, 1, False, 2) == 1 11 | assert test(False, 1, True, 2) == 2 12 | assert test(False, 1, False, 2, True, 3) == 3 13 | 14 | 15 | def test_Utilities__test_2(): 16 | """If the above is not met, and there is an extra argument 17 | it returns it.""" 18 | assert test(False, 1, False, 2, 3) == 3 19 | assert test(False, 1, 2) == 2 20 | assert test(1) == 1 21 | assert not test(False) 22 | 23 | 24 | def test_Utilities__test_3(): 25 | """It returns None if there are only False args followed by something.""" 26 | assert test(False, 1) is None 27 | assert test(False, 1, False, 2) is None 28 | 29 | 30 | def test_Utilities__reorder_1(): 31 | """It also supports 2-tuples containing key, value.""" 32 | s = [('k1', 'v1'), ('k2', 'v2'), ('k3', 'v3')] 33 | _with = [('k2', 'v2'), ('k3', 'v3')] 34 | without = [('k2', 'v2'), ('k4', 'v4')] 35 | assert reorder(s, _with, without) == [('k3', 'v3')] 36 | 37 | 38 | def test_Utilities_string_Formatter(): 39 | """Access to ``string.Formatter`` is denied.""" 40 | string = utility_builtins["string"] 41 | # access successful in principle 42 | assert string.ascii_lowercase == 'abcdefghijklmnopqrstuvwxyz' 43 | with pytest.raises(NotImplementedError) as exc: 44 | string.Formatter 45 | assert 'string.Formatter is not safe' == str(exc.value) 46 | -------------------------------------------------------------------------------- /tests/test_imports.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests about imports 3 | """ 4 | 5 | import pytest 6 | 7 | from RestrictedPython import compile_restricted_exec 8 | from RestrictedPython import safe_builtins 9 | from tests.helper import restricted_exec 10 | 11 | 12 | OS_IMPORT_EXAMPLE = """ 13 | import os 14 | 15 | os.listdir('/') 16 | """ 17 | 18 | 19 | def test_os_import(): 20 | """It does not allow to import anything by default. 21 | 22 | The `__import__` function is not provided as it is not safe. 23 | """ 24 | # Caution: This test is broken on PyPy until the following issue is fixed: 25 | # https://bitbucket.org/pypy/pypy/issues/2653 26 | # PyPy currently ignores the restriction of the `__builtins__`. 27 | glb = {'__builtins__': safe_builtins} 28 | 29 | with pytest.raises(ImportError) as err: 30 | restricted_exec(OS_IMPORT_EXAMPLE, glb) 31 | assert '__import__ not found' == str(err.value) 32 | 33 | 34 | BUILTINS_EXAMPLE = """ 35 | import builtins 36 | 37 | mygetattr = builtins['getattr'] 38 | """ 39 | 40 | 41 | def test_import_py3_builtins(): 42 | """It should not be allowed to access global builtins in Python3.""" 43 | result = compile_restricted_exec(BUILTINS_EXAMPLE) 44 | assert result.code is None 45 | assert result.errors == ( 46 | 'Line 2: "builtins" is a reserved name.', 47 | 'Line 4: "builtins" is a reserved name.' 48 | ) 49 | 50 | 51 | BUILTINS_AS_EXAMPLE = """ 52 | import builtins as glb 53 | 54 | mygetattr = glb['getattr'] 55 | """ 56 | 57 | 58 | def test_import_py3_as_builtins(): 59 | """It should not be allowed to access global builtins in Python3.""" 60 | result = compile_restricted_exec(BUILTINS_AS_EXAMPLE) 61 | assert result.code is None 62 | assert result.errors == ('Line 2: "builtins" is a reserved name.',) 63 | -------------------------------------------------------------------------------- /tests/transformer/test_augassign.py: -------------------------------------------------------------------------------- 1 | from RestrictedPython import compile_restricted_exec 2 | from tests.helper import restricted_exec 3 | 4 | 5 | def test_RestrictingNodeTransformer__visit_AugAssign__1( 6 | mocker): 7 | """It allows augmented assign for variables.""" 8 | _inplacevar_ = mocker.stub() 9 | _inplacevar_.side_effect = lambda op, val, expr: val + expr 10 | 11 | glb = { 12 | '_inplacevar_': _inplacevar_, 13 | 'a': 1, 14 | 'x': 1, 15 | 'z': 0 16 | } 17 | 18 | restricted_exec("a += x + z", glb) 19 | assert glb['a'] == 2 20 | _inplacevar_.assert_called_once_with('+=', 1, 1) 21 | _inplacevar_.reset_mock() 22 | 23 | 24 | def test_RestrictingNodeTransformer__visit_AugAssign__2(): 25 | """It forbids augmented assign of attributes.""" 26 | result = compile_restricted_exec("a.a += 1") 27 | assert result.errors == ( 28 | 'Line 1: Augmented assignment of attributes is not allowed.',) 29 | 30 | 31 | def test_RestrictingNodeTransformer__visit_AugAssign__3(): 32 | """It forbids augmented assign of subscripts.""" 33 | result = compile_restricted_exec("a[a] += 1") 34 | assert result.errors == ( 35 | 'Line 1: Augmented assignment of object items and slices is not ' 36 | 'allowed.',) 37 | 38 | 39 | def test_RestrictingNodeTransformer__visit_AugAssign__4(): 40 | """It forbids augmented assign of slices.""" 41 | result = compile_restricted_exec("a[x:y] += 1") 42 | assert result.errors == ( 43 | 'Line 1: Augmented assignment of object items and slices is not ' 44 | 'allowed.',) 45 | 46 | 47 | def test_RestrictingNodeTransformer__visit_AugAssign__5(): 48 | """It forbids augmented assign of slices with steps.""" 49 | result = compile_restricted_exec("a[x:y:z] += 1") 50 | assert result.errors == ( 51 | 'Line 1: Augmented assignment of object items and slices is not ' 52 | 'allowed.',) 53 | -------------------------------------------------------------------------------- /tests/test_NamedExpr.py: -------------------------------------------------------------------------------- 1 | """Assignment expression (``NamedExpr``) tests.""" 2 | 3 | 4 | from ast import NodeTransformer 5 | from ast import parse 6 | from unittest import TestCase 7 | 8 | from RestrictedPython import compile_restricted 9 | from RestrictedPython import safe_globals 10 | 11 | 12 | class TestNamedExpr(TestCase): 13 | def test_works(self): 14 | code, gs = compile_str("if x:= x + 1: True\n") 15 | gs["x"] = 0 16 | exec(code, gs) 17 | self.assertEqual(gs["x"], 1) 18 | 19 | def test_no_private_target(self): 20 | with self.assertRaises(SyntaxError): 21 | compile_str("if _x_:= 1: True\n") 22 | 23 | def test_simple_only(self): 24 | # we test here that only a simple variable is allowed 25 | # as assignemt expression target 26 | # Currently (Python 3.8, 3.9), this is enforced by the 27 | # Python concrete syntax; therefore, some (``ast``) trickery is 28 | # necessary to produce a test for it. 29 | class TransformNamedExprTarget(NodeTransformer): 30 | def visit_NamedExpr(self, node): 31 | # this is brutal but sufficient for the test 32 | node.target = None 33 | return node 34 | 35 | mod = parse("if x:= x + 1: True\n") 36 | mod = TransformNamedExprTarget().visit(mod) 37 | with self.assertRaisesRegex( 38 | SyntaxError, 39 | "Assignment expressions are only allowed for simple target"): 40 | code, gs = compile_str(mod) 41 | 42 | 43 | def compile_str(s, name=""): 44 | """code and globals for *s*. 45 | 46 | *s* must be acceptable for ``compile_restricted`` (this is (especially) the 47 | case for an ``str`` or ``ast.Module``). 48 | 49 | *name* is a ``str`` used in error messages. 50 | """ 51 | code = compile_restricted(s, name, 'exec') 52 | gs = safe_globals.copy() 53 | gs["__debug__"] = True # assert active 54 | return code, gs 55 | -------------------------------------------------------------------------------- /src/RestrictedPython/__init__.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2002 Zope Foundation and Contributors. 4 | # 5 | # This software is subject to the provisions of the Zope Public License, 6 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 7 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 8 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 9 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 10 | # FOR A PARTICULAR PURPOSE 11 | # 12 | ############################################################################## 13 | """RestrictedPython package.""" 14 | 15 | # flake8: NOQA: E401 16 | 17 | # This is a file to define public API in the base namespace of the package. 18 | # use: isort:skip to supress all isort related warnings / errors, 19 | # as this file should be logically grouped imports 20 | 21 | # compile_restricted methods: 22 | from RestrictedPython.compile import compile_restricted # isort:skip 23 | from RestrictedPython.compile import compile_restricted_eval # isort:skip 24 | from RestrictedPython.compile import compile_restricted_exec # isort:skip 25 | from RestrictedPython.compile import compile_restricted_function # isort:skip 26 | from RestrictedPython.compile import compile_restricted_single # isort:skip 27 | 28 | # predefined builtins 29 | from RestrictedPython.Guards import safe_builtins # isort:skip 30 | from RestrictedPython.Guards import safe_globals # isort:skip 31 | from RestrictedPython.Limits import limited_builtins # isort:skip 32 | from RestrictedPython.Utilities import utility_builtins # isort:skip 33 | 34 | # Helper Methods 35 | from RestrictedPython.PrintCollector import PrintCollector # isort:skip 36 | from RestrictedPython.compile import CompileResult # isort:skip 37 | 38 | # Policy 39 | from RestrictedPython.transformer import RestrictingNodeTransformer # isort:skip 40 | 41 | # 42 | from RestrictedPython.Eval import RestrictionCapableEval 43 | -------------------------------------------------------------------------------- /src/RestrictedPython/Limits.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2002 Zope Foundation and Contributors. 4 | # 5 | # This software is subject to the provisions of the Zope Public License, 6 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 7 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 8 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 9 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 10 | # FOR A PARTICULAR PURPOSE 11 | # 12 | ############################################################################## 13 | 14 | limited_builtins = {} 15 | 16 | 17 | def limited_range(iFirst, *args): 18 | # limited range function from Martijn Pieters 19 | RANGELIMIT = 1000 20 | if not len(args): 21 | iStart, iEnd, iStep = 0, iFirst, 1 22 | elif len(args) == 1: 23 | iStart, iEnd, iStep = iFirst, args[0], 1 24 | elif len(args) == 2: 25 | iStart, iEnd, iStep = iFirst, args[0], args[1] 26 | else: 27 | raise AttributeError('range() requires 1-3 int arguments') 28 | if iStep == 0: 29 | raise ValueError('zero step for range()') 30 | iLen = int((iEnd - iStart) / iStep) 31 | if iLen < 0: 32 | iLen = 0 33 | if iLen >= RANGELIMIT: 34 | raise ValueError( 35 | 'To be created range() object would be to large, ' 36 | 'in RestrictedPython we only allow {limit} ' 37 | 'elements in a range.'.format(limit=str(RANGELIMIT)), 38 | ) 39 | return range(iStart, iEnd, iStep) 40 | 41 | 42 | limited_builtins['range'] = limited_range 43 | 44 | 45 | def limited_list(seq): 46 | if isinstance(seq, str): 47 | raise TypeError('cannot convert string to list') 48 | return list(seq) 49 | 50 | 51 | limited_builtins['list'] = limited_list 52 | 53 | 54 | def limited_tuple(seq): 55 | if isinstance(seq, str): 56 | raise TypeError('cannot convert string to tuple') 57 | return tuple(seq) 58 | 59 | 60 | limited_builtins['tuple'] = limited_tuple 61 | -------------------------------------------------------------------------------- /tests/transformer/test_inspect.py: -------------------------------------------------------------------------------- 1 | from RestrictedPython import compile_restricted_exec 2 | 3 | 4 | def test_get_inspect_frame_on_generator(): 5 | source_code = """ 6 | generator = (statement.gi_frame for _ in (1,)) 7 | generator_element = [elem for elem in generator][0] 8 | 9 | """ 10 | result = compile_restricted_exec(source_code) 11 | assert result.errors == ( 12 | 'Line 2: "gi_frame" is a restricted name, ' 13 | 'that is forbidden to access in RestrictedPython.', 14 | ) 15 | 16 | 17 | def test_get_inspect_frame_back_on_generator(): 18 | source_code = """ 19 | generator = (statement.gi_frame.f_back.f_back for _ in (1,)) 20 | generator_element = [elem for elem in generator][0] 21 | 22 | """ 23 | result = compile_restricted_exec(source_code) 24 | assert result.errors == ( 25 | 'Line 2: "f_back" is a restricted name, ' 26 | 'that is forbidden to access in RestrictedPython.', 27 | 'Line 2: "f_back" is a restricted name, ' 28 | 'that is forbidden to access in RestrictedPython.', 29 | 'Line 2: "gi_frame" is a restricted name, ' 30 | 'that is forbidden to access in RestrictedPython.', 31 | ) 32 | 33 | 34 | def test_call_inspect_frame_on_generator(): 35 | source_code = """ 36 | generator = None 37 | frame = None 38 | 39 | def test(): 40 | global generator, frame 41 | frame = g.gi_frame.f_back.f_back 42 | yield frame 43 | 44 | generator = test() 45 | generator.send(None) 46 | os = frame.f_builtins.get('__import__')('os') 47 | 48 | result = os.listdir('/') 49 | """ 50 | result = compile_restricted_exec(source_code) 51 | assert result.errors == ( 52 | 'Line 7: "f_back" is a restricted name, ' 53 | 'that is forbidden to access in RestrictedPython.', 54 | 'Line 7: "f_back" is a restricted name, ' 55 | 'that is forbidden to access in RestrictedPython.', 56 | 'Line 7: "gi_frame" is a restricted name, ' 57 | 'that is forbidden to access in RestrictedPython.', 58 | 'Line 12: "f_builtins" is a restricted name, ' 59 | 'that is forbidden to access in RestrictedPython.', 60 | ) 61 | -------------------------------------------------------------------------------- /tests/builtins/test_limits.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from RestrictedPython.Limits import limited_list 4 | from RestrictedPython.Limits import limited_range 5 | from RestrictedPython.Limits import limited_tuple 6 | 7 | 8 | def test_limited_range_length_1(): 9 | result = limited_range(1) 10 | assert result == range(0, 1) 11 | 12 | 13 | def test_limited_range_length_10(): 14 | result = limited_range(10) 15 | assert result == range(0, 10) 16 | 17 | 18 | def test_limited_range_5_10(): 19 | result = limited_range(5, 10) 20 | assert result == range(5, 10) 21 | 22 | 23 | def test_limited_range_5_10_sm1(): 24 | result = limited_range(5, 10, -1) 25 | assert result == range(5, 10, -1) 26 | 27 | 28 | def test_limited_range_15_10_s2(): 29 | result = limited_range(15, 10, 2) 30 | assert result == range(15, 10, 2) 31 | 32 | 33 | def test_limited_range_no_input(): 34 | with pytest.raises(TypeError): 35 | limited_range() 36 | 37 | 38 | def test_limited_range_more_steps(): 39 | with pytest.raises(AttributeError): 40 | limited_range(0, 0, 0, 0) 41 | 42 | 43 | def test_limited_range_zero_step(): 44 | with pytest.raises(ValueError): 45 | limited_range(0, 10, 0) 46 | 47 | 48 | def test_limited_range_range_overflow(): 49 | with pytest.raises(ValueError) as excinfo: 50 | limited_range(0, 5000, 1) 51 | assert ( 52 | 'To be created range() object would be to large, ' 53 | 'in RestrictedPython we only allow 1000 elements in a range.' 54 | in str(excinfo.value) 55 | ) 56 | 57 | 58 | def test_limited_list_valid_list_input(): 59 | input = [1, 2, 3] 60 | result = limited_list(input) 61 | assert result == input 62 | 63 | 64 | def test_limited_list_invalid_string_input(): 65 | with pytest.raises(TypeError): 66 | limited_list('input') 67 | 68 | 69 | def test_limited_tuple_valid_list_input(): 70 | input = [1, 2, 3] 71 | result = limited_tuple(input) 72 | assert result == tuple(input) 73 | 74 | 75 | def test_limited_tuple_invalid_string_input(): 76 | with pytest.raises(TypeError): 77 | limited_tuple('input') 78 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Zope Public License (ZPL) Version 2.1 2 | 3 | A copyright notice accompanies this license document that identifies the 4 | copyright holders. 5 | 6 | This license has been certified as open source. It has also been designated as 7 | GPL compatible by the Free Software Foundation (FSF). 8 | 9 | Redistribution and use in source and binary forms, with or without 10 | modification, are permitted provided that the following conditions are met: 11 | 12 | 1. Redistributions in source code must retain the accompanying copyright 13 | notice, this list of conditions, and the following disclaimer. 14 | 15 | 2. Redistributions in binary form must reproduce the accompanying copyright 16 | notice, this list of conditions, and the following disclaimer in the 17 | documentation and/or other materials provided with the distribution. 18 | 19 | 3. Names of the copyright holders must not be used to endorse or promote 20 | products derived from this software without prior written permission from the 21 | copyright holders. 22 | 23 | 4. The right to distribute this software or to use it for any purpose does not 24 | give you the right to use Servicemarks (sm) or Trademarks (tm) of the 25 | copyright 26 | holders. Use of them is covered by separate agreement with the copyright 27 | holders. 28 | 29 | 5. If any files are modified, you must cause the modified files to carry 30 | prominent notices stating that you changed the files and the date of any 31 | change. 32 | 33 | Disclaimer 34 | 35 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY EXPRESSED 36 | OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 37 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 38 | EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT, 39 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 40 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 41 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 42 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 43 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 44 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 45 | -------------------------------------------------------------------------------- /tests/transformer/test_yield.py: -------------------------------------------------------------------------------- 1 | from RestrictedPython import compile_restricted_exec 2 | 3 | 4 | YIELD_EXAMPLE = """\ 5 | def test_generator(): 6 | yield 42 7 | """ 8 | 9 | 10 | def test_yield(): 11 | """`yield` statement should be allowed.""" 12 | result = compile_restricted_exec(YIELD_EXAMPLE) 13 | assert result.errors == () 14 | assert result.code is not None 15 | local = {} 16 | exec(result.code, {}, local) 17 | test_generator = local['test_generator'] 18 | exec_result = list(test_generator()) 19 | assert exec_result == [42] 20 | 21 | 22 | YIELD_FORM_EXAMPLE = """ 23 | def reader_wapper(input): 24 | yield from input 25 | """ 26 | 27 | 28 | def test_yield_from(): 29 | """`yield from` statement should be allowed.""" 30 | result = compile_restricted_exec(YIELD_FORM_EXAMPLE) 31 | assert result.errors == () 32 | assert result.code is not None 33 | 34 | def my_external_generator(): 35 | my_list = [1, 2, 3, 4, 5] 36 | yield from my_list 37 | 38 | local = {} 39 | exec(result.code, {}, local) 40 | reader_wapper = local['reader_wapper'] 41 | exec_result = list(reader_wapper(my_external_generator())) 42 | assert exec_result == [1, 2, 3, 4, 5] 43 | 44 | 45 | # Modified Example from http://stackabuse.com/python-async-await-tutorial/ 46 | ASYNCIO_YIELD_FORM_EXAMPLE = """ 47 | import asyncio 48 | 49 | @asyncio.coroutine 50 | def get_json(client, url): 51 | file_content = yield from load_file('data.ini') 52 | """ 53 | 54 | 55 | def test_asyncio_yield_from(): 56 | """`yield from` statement should be allowed.""" 57 | result = compile_restricted_exec(ASYNCIO_YIELD_FORM_EXAMPLE) 58 | assert result.errors == () 59 | assert result.code is not None 60 | 61 | 62 | ASYNC_YIELD_FORM_EXAMPLE = """ 63 | import asyncio 64 | 65 | async def get_json(client, url): 66 | file_content = yield from load_file('data.ini') 67 | """ 68 | 69 | 70 | def test_async_yield_from(): 71 | """`yield from` statement should be allowed.""" 72 | result = compile_restricted_exec(ASYNC_YIELD_FORM_EXAMPLE) 73 | assert result.errors == ( 74 | 'Line 4: AsyncFunctionDef statements are not allowed.', 75 | ) 76 | assert result.code is None 77 | -------------------------------------------------------------------------------- /tests/transformer/test_lambda.py: -------------------------------------------------------------------------------- 1 | from RestrictedPython import compile_restricted_exec 2 | from tests.helper import restricted_exec 3 | 4 | 5 | lambda_err_msg = 'Line 1: "_bad" is an invalid variable ' \ 6 | 'name because it starts with "_"' 7 | 8 | 9 | def test_RestrictingNodeTransformer__visit_Lambda__1(): 10 | """It prevents arguments starting with `_`.""" 11 | result = compile_restricted_exec("lambda _bad: None") 12 | assert result.errors == (lambda_err_msg,) 13 | 14 | 15 | def test_RestrictingNodeTransformer__visit_Lambda__2(): 16 | """It prevents keyword arguments starting with `_`.""" 17 | result = compile_restricted_exec("lambda _bad=1: None") 18 | assert result.errors == (lambda_err_msg,) 19 | 20 | 21 | def test_RestrictingNodeTransformer__visit_Lambda__3(): 22 | """It prevents * arguments starting with `_`.""" 23 | result = compile_restricted_exec("lambda *_bad: None") 24 | assert result.errors == (lambda_err_msg,) 25 | 26 | 27 | def test_RestrictingNodeTransformer__visit_Lambda__4(): 28 | """It prevents ** arguments starting with `_`.""" 29 | result = compile_restricted_exec("lambda **_bad: None") 30 | assert result.errors == (lambda_err_msg,) 31 | 32 | 33 | def test_RestrictingNodeTransformer__visit_Lambda__7(): 34 | """It prevents arguments starting with `_` together with a single `*`.""" 35 | result = compile_restricted_exec("lambda good, *, _bad: None") 36 | assert result.errors == (lambda_err_msg,) 37 | 38 | 39 | BAD_ARG_IN_LAMBDA = """\ 40 | def check_getattr_in_lambda(arg=lambda _bad=(lambda ob, name: name): _bad2): 41 | 42 42 | """ 43 | 44 | 45 | def test_RestrictingNodeTransformer__visit_Lambda__8(): 46 | """It prevents arguments starting with `_` in weird lambdas.""" 47 | result = compile_restricted_exec(BAD_ARG_IN_LAMBDA) 48 | assert lambda_err_msg in result.errors 49 | 50 | 51 | LAMBDA_FUNC_1 = """ 52 | g = lambda x: x ** 2 53 | """ 54 | 55 | 56 | def test_RestrictingNodeTransformer__visit_Lambda__10(): 57 | """Simple lambda functions are allowed.""" 58 | restricted_globals = dict( 59 | g=None, 60 | ) 61 | restricted_exec(LAMBDA_FUNC_1, restricted_globals) 62 | assert 4 == restricted_globals['g'](2) 63 | -------------------------------------------------------------------------------- /tests/transformer/test_functiondef.py: -------------------------------------------------------------------------------- 1 | from RestrictedPython import compile_restricted_exec 2 | 3 | 4 | functiondef_err_msg = 'Line 1: "_bad" is an invalid variable ' \ 5 | 'name because it starts with "_"' 6 | 7 | 8 | def test_RestrictingNodeTransformer__visit_FunctionDef__1(): 9 | """It prevents function arguments starting with `_`.""" 10 | result = compile_restricted_exec("def foo(_bad): pass") 11 | assert result.errors == (functiondef_err_msg,) 12 | 13 | 14 | def test_RestrictingNodeTransformer__visit_FunctionDef__2(): 15 | """It prevents function keyword arguments starting with `_`.""" 16 | result = compile_restricted_exec("def foo(_bad=1): pass") 17 | assert result.errors == (functiondef_err_msg,) 18 | 19 | 20 | def test_RestrictingNodeTransformer__visit_FunctionDef__3(): 21 | """It prevents function * arguments starting with `_`.""" 22 | result = compile_restricted_exec("def foo(*_bad): pass") 23 | assert result.errors == (functiondef_err_msg,) 24 | 25 | 26 | def test_RestrictingNodeTransformer__visit_FunctionDef__4(): 27 | """It prevents function ** arguments starting with `_`.""" 28 | result = compile_restricted_exec("def foo(**_bad): pass") 29 | assert result.errors == (functiondef_err_msg,) 30 | 31 | 32 | def test_RestrictingNodeTransformer__visit_FunctionDef__7(): 33 | """It prevents `_` function arguments together with a single `*`.""" 34 | result = compile_restricted_exec("def foo(good, *, _bad): pass") 35 | assert result.errors == (functiondef_err_msg,) 36 | 37 | 38 | BLACKLISTED_FUNC_NAMES_CALL_TEST = """ 39 | def __init__(test): 40 | test 41 | 42 | __init__(1) 43 | """ 44 | 45 | 46 | def test_RestrictingNodeTransformer__module_func_def_name_call(): 47 | """It forbids definition and usage of magic methods as functions ... 48 | 49 | ... at module level. 50 | """ 51 | result = compile_restricted_exec(BLACKLISTED_FUNC_NAMES_CALL_TEST) 52 | # assert result.errors == ('Line 1: ') 53 | assert result.errors == ( 54 | 'Line 2: "__init__" is an invalid variable name because it starts with "_"', # NOQA: E501 55 | 'Line 5: "__init__" is an invalid variable name because it starts with "_"', # NOQA: E501 56 | ) 57 | -------------------------------------------------------------------------------- /tests/transformer/test_comparators.py: -------------------------------------------------------------------------------- 1 | from tests.helper import restricted_eval 2 | 3 | 4 | def test_RestrictingNodeTransformer__visit_Eq__1(): 5 | """It allows == expressions.""" 6 | assert restricted_eval('1 == int("1")') is True 7 | 8 | 9 | def test_RestrictingNodeTransformer__visit_NotEq__1(): 10 | """It allows != expressions.""" 11 | assert restricted_eval('1 != int("1")') is False 12 | 13 | 14 | def test_RestrictingNodeTransformer__visit_Lt__1(): 15 | """It allows < expressions.""" 16 | assert restricted_eval('1 < 3') is True 17 | 18 | 19 | def test_RestrictingNodeTransformer__visit_LtE__1(): 20 | """It allows < expressions.""" 21 | assert restricted_eval('1 <= 3') is True 22 | 23 | 24 | def test_RestrictingNodeTransformer__visit_Gt__1(): 25 | """It allows > expressions.""" 26 | assert restricted_eval('1 > 3') is False 27 | 28 | 29 | def test_RestrictingNodeTransformer__visit_GtE__1(): 30 | """It allows >= expressions.""" 31 | assert restricted_eval('1 >= 3') is False 32 | 33 | 34 | def test_RestrictingNodeTransformer__visit_Is__1(): 35 | """It allows `is` expressions.""" 36 | assert restricted_eval('None is None') is True 37 | 38 | 39 | def test_RestrictingNodeTransformer__visit_IsNot__1(): 40 | """It allows `is not` expressions.""" 41 | assert restricted_eval('2 is not None') is True 42 | 43 | 44 | def test_RestrictingNodeTransformer__visit_In_List(): 45 | """It allows `in` expressions for lists.""" 46 | assert restricted_eval('2 in [1, 2, 3]') is True 47 | 48 | 49 | def test_RestrictingNodeTransformer__visit_NotIn_List(): 50 | """It allows `not in` expressions for lists.""" 51 | assert restricted_eval('2 not in [1, 2, 3]') is False 52 | 53 | 54 | def test_RestrictingNodeTransformer__visit_In_Set(): 55 | """It allows `in` expressions for sets.""" 56 | assert restricted_eval('2 in {1, 1, 2, 3}') is True 57 | 58 | 59 | def test_RestrictingNodeTransformer__visit_NotIn_Set(): 60 | """It allows `not in` expressions for sets.""" 61 | assert restricted_eval('2 not in {1, 2, 3}') is False 62 | 63 | 64 | def test_RestrictingNodeTransformer__visit_In_Dict(): 65 | """It allows `in` expressions for dicts.""" 66 | assert restricted_eval('2 in {1: 1, 2: 2, 3: 3}') is True 67 | 68 | 69 | def test_RestrictingNodeTransformer__visit_NotIn_Dict(): 70 | """It allows `not in` expressions for dicts.""" 71 | assert restricted_eval('2 not in {1: 1, 2: 2, 3: 3}') is False 72 | -------------------------------------------------------------------------------- /.meta.toml: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | [meta] 4 | template = "pure-python" 5 | commit-id = "72252845" 6 | 7 | [python] 8 | with-pypy = false 9 | with-docs = true 10 | with-sphinx-doctests = true 11 | with-windows = true 12 | with-future-python = true 13 | with-macos = false 14 | 15 | [tox] 16 | use-flake8 = true 17 | additional-envlist = [ 18 | "py311-datetime", 19 | "combined-coverage", 20 | ] 21 | testenv-deps = [ 22 | "datetime: DateTime", 23 | "-cconstraints.txt", 24 | "pytest-cov", 25 | ] 26 | testenv-setenv = [ 27 | "COVERAGE_FILE=.coverage.{envname}", 28 | ] 29 | testenv-commands = [ 30 | "python -V", 31 | "pytest --cov=src --cov=tests --cov-report= tests {posargs}", 32 | ] 33 | testenv-additional = [ 34 | "", 35 | "[testenv:combined-coverage]", 36 | "basepython = python3", 37 | "allowlist_externals =", 38 | " mkdir", 39 | "deps =", 40 | " coverage", 41 | " -cconstraints.txt", 42 | "setenv =", 43 | " COVERAGE_FILE=.coverage", 44 | "commands =", 45 | " mkdir -p {toxinidir}/parts/htmlcov", 46 | " coverage erase", 47 | " coverage combine", 48 | " coverage html", 49 | " coverage report -m --fail-under=100", 50 | "depends = py39,py310,py311,py311-datetime,py312,py313,py314,coverage", 51 | ] 52 | coverage-command = "pytest --cov=src --cov=tests --cov-report= tests {posargs}" 53 | coverage-setenv = [ 54 | "COVERAGE_FILE=.coverage", 55 | ] 56 | 57 | [coverage] 58 | fail-under = 97.2 59 | 60 | [isort] 61 | additional-sources = "{toxinidir}/tests" 62 | 63 | [flake8] 64 | additional-sources = "tests" 65 | 66 | [manifest] 67 | additional-rules = [ 68 | "include *.yaml", 69 | "recursive-include docs *.ast", 70 | "recursive-include docs *.bat", 71 | "recursive-include docs *.jpg", 72 | "recursive-include tests *.py", 73 | ] 74 | 75 | [check-manifest] 76 | additional-ignores = [ 77 | "docs/CHANGES.rst", 78 | "docs/_build/html/_images/*", 79 | "docs/_build/html/_sources/contributing/*", 80 | "docs/_build/html/_sources/install/*", 81 | "docs/_build/html/_sources/roadmap/*", 82 | "docs/_build/html/_sources/upgrade_dependencies/*", 83 | "docs/_build/html/_sources/usage/*", 84 | "docs/_build/html/_static/scripts/*", 85 | ] 86 | 87 | [github-actions] 88 | additional-config = [ 89 | "- [\"3.11\", \"py311-datetime\"]", 90 | ] 91 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | name: tests 4 | 5 | on: 6 | push: 7 | pull_request: 8 | schedule: 9 | - cron: '0 12 * * 0' # run once a week on Sunday 10 | # Allow to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | permissions: 16 | contents: read 17 | pull-requests: write 18 | strategy: 19 | # We want to see all failures: 20 | fail-fast: false 21 | matrix: 22 | os: 23 | - ["ubuntu", "ubuntu-latest"] 24 | - ["windows", "windows-latest"] 25 | config: 26 | # [Python version, tox env] 27 | - ["3.11", "release-check"] 28 | - ["3.9", "py39"] 29 | - ["3.10", "py310"] 30 | - ["3.11", "py311"] 31 | - ["3.12", "py312"] 32 | - ["3.13", "py313"] 33 | - ["3.14", "py314"] 34 | - ["3.11", "docs"] 35 | - ["3.11", "coverage"] 36 | - ["3.11", "py311-datetime"] 37 | exclude: 38 | - { os: ["windows", "windows-latest"], config: ["3.11", "release-check"] } 39 | - { os: ["windows", "windows-latest"], config: ["3.11", "docs"] } 40 | - { os: ["windows", "windows-latest"], config: ["3.11", "coverage"] } 41 | 42 | runs-on: ${{ matrix.os[1] }} 43 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name 44 | name: ${{ matrix.os[0] }}-${{ matrix.config[1] }} 45 | steps: 46 | - uses: actions/checkout@v5 47 | with: 48 | persist-credentials: false 49 | - name: Install uv + caching 50 | # astral/setup-uv@7 51 | uses: astral-sh/setup-uv@3259c6206f993105e3a61b142c2d97bf4b9ef83d 52 | with: 53 | enable-cache: true 54 | cache-dependency-glob: | 55 | setup.* 56 | tox.ini 57 | python-version: ${{ matrix.config[0] }} 58 | github-token: ${{ secrets.GITHUB_TOKEN }} 59 | - name: Test 60 | if: ${{ !startsWith(runner.os, 'Mac') }} 61 | run: uvx --with tox-uv tox -e ${{ matrix.config[1] }} 62 | - name: Test (macOS) 63 | if: ${{ startsWith(runner.os, 'Mac') }} 64 | run: uvx --with tox-uv tox -e ${{ matrix.config[1] }}-universal2 65 | - name: Coverage 66 | if: matrix.config[1] == 'coverage' 67 | run: | 68 | uvx coveralls --service=github 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2006 Zope Foundation and Contributors. 4 | # All Rights Reserved. 5 | # 6 | # This software is subject to the provisions of the Zope Public License, 7 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 8 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 9 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 10 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 11 | # FOR A PARTICULAR PURPOSE. 12 | # 13 | ############################################################################## 14 | """Setup for RestrictedPython package""" 15 | 16 | import os 17 | 18 | from setuptools import find_packages 19 | from setuptools import setup 20 | 21 | 22 | def read(*rnames): 23 | with open(os.path.join(os.path.dirname(__file__), *rnames)) as f: 24 | return f.read() 25 | 26 | 27 | setup(name='RestrictedPython', 28 | version='8.2.dev0', 29 | url='https://github.com/zopefoundation/RestrictedPython', 30 | license='ZPL-2.1', 31 | description=( 32 | 'RestrictedPython is a defined subset of the Python language which ' 33 | 'allows to provide a program input into a trusted environment.'), 34 | long_description=read('README.rst') + '\n' + read('CHANGES.rst'), 35 | long_description_content_type='text/x-rst', 36 | classifiers=[ 37 | 'Development Status :: 6 - Mature', 38 | 'License :: OSI Approved :: Zope Public License', 39 | 'Programming Language :: Python', 40 | 'Operating System :: OS Independent', 41 | 'Programming Language :: Python :: 3', 42 | 'Programming Language :: Python :: 3.9', 43 | 'Programming Language :: Python :: 3.10', 44 | 'Programming Language :: Python :: 3.11', 45 | 'Programming Language :: Python :: 3.12', 46 | 'Programming Language :: Python :: 3.13', 47 | 'Programming Language :: Python :: 3.14', 48 | 'Programming Language :: Python :: Implementation :: CPython', 49 | 'Topic :: Security', 50 | ], 51 | keywords='restricted execution security untrusted code', 52 | author='Zope Foundation and Contributors', 53 | author_email='zope-dev@zope.dev', 54 | project_urls={ 55 | "Documentation": "https://restrictedpython.readthedocs.io/", 56 | "Source": "https://github.com/zopefoundation/RestrictedPython", 57 | "Tracker": 58 | "https://github.com/zopefoundation/RestrictedPython/issues", 59 | }, 60 | packages=find_packages('src'), 61 | package_dir={'': 'src'}, 62 | install_requires=[], 63 | python_requires=">=3.9, <3.15", 64 | extras_require={ 65 | 'test': ['pytest', 'pytest-mock'], 66 | 'docs': ['Sphinx', 'furo'], 67 | }, 68 | include_package_data=True, 69 | zip_safe=False) 70 | -------------------------------------------------------------------------------- /tests/transformer/test_async.py: -------------------------------------------------------------------------------- 1 | from RestrictedPython import compile_restricted_exec 2 | from RestrictedPython.transformer import RestrictingNodeTransformer 3 | 4 | 5 | # Example from https://docs.python.org/3/library/asyncio-task.html 6 | ASYNC_DEF_EXMAPLE = """ 7 | import asyncio 8 | 9 | async def hello_world(): 10 | print() 11 | 12 | loop = asyncio.get_event_loop() 13 | # Blocking call which returns when the hello_world() coroutine is done 14 | loop.run_until_complete(hello_world()) 15 | loop.close() 16 | """ 17 | 18 | 19 | def test_async_def(): 20 | result = compile_restricted_exec(ASYNC_DEF_EXMAPLE) 21 | assert result.errors == ( 22 | 'Line 4: AsyncFunctionDef statements are not allowed.', 23 | ) 24 | assert result.code is None 25 | 26 | 27 | class RestrictingAsyncNodeTransformer(RestrictingNodeTransformer): 28 | """Transformer which allows `async def` for the tests.""" 29 | 30 | def visit_AsyncFunctionDef(self, node): 31 | """Allow `async def`. 32 | 33 | This is needed to get the function body to be parsed thus allowing 34 | to catch `await`, `async for` and `async with`. 35 | """ 36 | return self.node_contents_visit(node) 37 | 38 | 39 | # Modified example from https://docs.python.org/3/library/asyncio-task.html 40 | AWAIT_EXAMPLE = """ 41 | import asyncio 42 | import datetime 43 | 44 | async def display_date(loop): 45 | end_time = loop.time() + 5.0 46 | while True: 47 | print(datetime.datetime.now()) 48 | if (loop.time() + 1.0) >= end_time: 49 | break 50 | await asyncio.sleep(1) 51 | 52 | loop = asyncio.get_event_loop() 53 | # Blocking call which returns when the display_date() coroutine is done 54 | loop.run_until_complete(display_date(loop)) 55 | loop.close() 56 | """ 57 | 58 | 59 | def test_await(): 60 | result = compile_restricted_exec( 61 | AWAIT_EXAMPLE, 62 | policy=RestrictingAsyncNodeTransformer) 63 | assert result.errors == ('Line 11: Await statements are not allowed.',) 64 | assert result.code is None 65 | 66 | 67 | # Modified example https://www.python.org/dev/peps/pep-0525/ 68 | ASYNC_WITH_EXAMPLE = """ 69 | async def square_series(con, to): 70 | async with con.transaction(): 71 | print(con) 72 | """ 73 | 74 | 75 | def test_async_with(): 76 | result = compile_restricted_exec( 77 | ASYNC_WITH_EXAMPLE, 78 | policy=RestrictingAsyncNodeTransformer) 79 | assert result.errors == ('Line 3: AsyncWith statements are not allowed.',) 80 | assert result.code is None 81 | 82 | 83 | # Modified example https://www.python.org/dev/peps/pep-0525/ 84 | ASYNC_FOR_EXAMPLE = """ 85 | async def read_rows(rows): 86 | async for row in rows: 87 | yield row 88 | """ 89 | 90 | 91 | def test_async_for(): 92 | result = compile_restricted_exec( 93 | ASYNC_FOR_EXAMPLE, 94 | policy=RestrictingAsyncNodeTransformer) 95 | assert result.errors == ('Line 3: AsyncFor statements are not allowed.',) 96 | assert result.code is None 97 | -------------------------------------------------------------------------------- /tests/transformer/test_tstring.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from RestrictedPython import compile_restricted_exec 4 | from RestrictedPython._compat import IS_PY314_OR_GREATER 5 | from RestrictedPython.Eval import default_guarded_getattr 6 | from RestrictedPython.Eval import default_guarded_getiter 7 | from RestrictedPython.PrintCollector import PrintCollector 8 | 9 | 10 | if IS_PY314_OR_GREATER: 11 | from string.templatelib import Template 12 | 13 | 14 | @pytest.mark.skipif( 15 | not IS_PY314_OR_GREATER, 16 | reason="t-strings were added in Python 3.14.", 17 | ) 18 | def test_transform(): 19 | """It compiles a function call successfully and returns the used name.""" 20 | 21 | result = compile_restricted_exec('a = t"{max([1, 2, 3])}"') 22 | assert result.errors == () 23 | assert result.warnings == [] 24 | assert result.code is not None 25 | loc = {} 26 | exec(result.code, {}, loc) 27 | template = loc['a'] 28 | assert isinstance(template, Template) 29 | assert template.values == (3, ) 30 | assert result.used_names == {'max': True} 31 | 32 | 33 | @pytest.mark.skipif( 34 | not IS_PY314_OR_GREATER, 35 | reason="t-strings were added in Python 3.14.", 36 | ) 37 | def test_visit_invalid_variable_name(): 38 | """Accessing private attributes is forbidden. 39 | 40 | This is just a smoke test to validate that restricted exec is used 41 | in the run-time evaluation of t-strings. 42 | """ 43 | result = compile_restricted_exec('t"{__init__}"') 44 | assert result.errors == ( 45 | 'Line 1: "__init__" is an invalid variable name because it starts with "_"', # NOQA: E501 46 | ) 47 | 48 | 49 | t_string_self_documenting_expressions_example = """ 50 | from datetime import date 51 | from string.templatelib import Template, Interpolation 52 | 53 | user = 'eric_idle' 54 | member_since = date(1975, 7, 31) 55 | 56 | def render_template(template: Template) -> str: 57 | result = '' 58 | for part in template: 59 | if isinstance(part, Interpolation): 60 | if isinstance(part.value, str): 61 | result += part.value.upper() 62 | else: 63 | result += str(part.value) 64 | else: 65 | result += part.lower() 66 | return result 67 | 68 | print(render_template(t'The User {user} is a member since {member_since}')) 69 | """ 70 | 71 | 72 | @pytest.mark.skipif( 73 | not IS_PY314_OR_GREATER, 74 | reason="t-strings were added in Python 3.14.", 75 | ) 76 | def test_t_string_self_documenting_expressions(): 77 | """Checks if t-string self-documenting expressions is checked.""" 78 | result = compile_restricted_exec( 79 | t_string_self_documenting_expressions_example, 80 | ) 81 | assert result.errors == () 82 | assert result.code is not None 83 | 84 | glb = { 85 | '_print_': PrintCollector, 86 | '_getattr_': default_guarded_getattr, 87 | '_getiter_': default_guarded_getiter, 88 | '_inplacevar_': lambda x, y, z: y + z, 89 | } 90 | exec(result.code, glb) 91 | assert glb['_print']() == "the user ERIC_IDLE is a member since 1975-07-31\n" # NOQA: E501 92 | -------------------------------------------------------------------------------- /tests/transformer/test_import.py: -------------------------------------------------------------------------------- 1 | from RestrictedPython import compile_restricted_exec 2 | 3 | 4 | import_errmsg = ( 5 | 'Line 1: "%s" is an invalid variable name because it starts with "_"') 6 | 7 | 8 | def test_RestrictingNodeTransformer__visit_Import__1(): 9 | """It allows importing a module.""" 10 | result = compile_restricted_exec('import a') 11 | assert result.errors == () 12 | assert result.code is not None 13 | 14 | 15 | def test_RestrictingNodeTransformer__visit_Import__2(): 16 | """It denies importing a module starting with `_`.""" 17 | result = compile_restricted_exec('import _a') 18 | assert result.errors == (import_errmsg % '_a',) 19 | 20 | 21 | def test_RestrictingNodeTransformer__visit_Import__3(): 22 | """It denies importing a module starting with `_` as something.""" 23 | result = compile_restricted_exec('import _a as m') 24 | assert result.errors == (import_errmsg % '_a',) 25 | 26 | 27 | def test_RestrictingNodeTransformer__visit_Import__4(): 28 | """It denies importing a module as something starting with `_`.""" 29 | result = compile_restricted_exec('import a as _m') 30 | assert result.errors == (import_errmsg % '_m',) 31 | 32 | 33 | def test_RestrictingNodeTransformer__visit_Import__5(): 34 | """It allows importing from a module.""" 35 | result = compile_restricted_exec('from a import m') 36 | assert result.errors == () 37 | assert result.code is not None 38 | 39 | 40 | def test_RestrictingNodeTransformer__visit_Import_6(): 41 | """It allows importing from a module starting with `_`.""" 42 | result = compile_restricted_exec('from _a import m') 43 | assert result.errors == () 44 | assert result.code is not None 45 | 46 | 47 | def test_RestrictingNodeTransformer__visit_Import__7(): 48 | """It denies importing from a module as something starting with `_`.""" 49 | result = compile_restricted_exec('from a import m as _n') 50 | assert result.errors == (import_errmsg % '_n',) 51 | 52 | 53 | def test_RestrictingNodeTransformer__visit_Import__8(): 54 | """It denies as-importing something starting with `_` from a module.""" 55 | result = compile_restricted_exec('from a import _m as n') 56 | assert result.errors == (import_errmsg % '_m',) 57 | 58 | 59 | def test_RestrictingNodeTransformer__visit_Import__9(): 60 | """It denies relative from importing as something starting with `_`.""" 61 | result = compile_restricted_exec('from .x import y as _leading_underscore') 62 | assert result.errors == (import_errmsg % '_leading_underscore',) 63 | 64 | 65 | def test_RestrictingNodeTransformer__visit_Import_star__1(): 66 | """Importing `*` is a SyntaxError in Python itself.""" 67 | result = compile_restricted_exec('import *') 68 | assert result.errors == ( 69 | "Line 1: SyntaxError: invalid syntax at statement: 'import *'",) 70 | assert result.code is None 71 | 72 | 73 | def test_RestrictingNodeTransformer__visit_Import_star__2(): 74 | """It denies importing `*` from a module.""" 75 | result = compile_restricted_exec('from a import *') 76 | assert result.errors == ('Line 1: "*" imports are not allowed.',) 77 | assert result.code is None 78 | -------------------------------------------------------------------------------- /docs/usage/framework_usage.rst: -------------------------------------------------------------------------------- 1 | .. _sec_usage_frameworks: 2 | 3 | Usage in frameworks and Zope 4 | ---------------------------- 5 | 6 | One major issue with using ``compile_restricted`` directly in a framework is, that you have to use try-except statements to handle problems and it might be a bit harder to provide useful information to the user. 7 | RestrictedPython provides four specialized compile_restricted methods: 8 | 9 | * ``compile_restricted_exec`` 10 | * ``compile_restricted_eval`` 11 | * ``compile_restricted_single`` 12 | * ``compile_restricted_function`` 13 | 14 | Those four methods return a named tuple (``CompileResult``) with four elements: 15 | 16 | ``code`` 17 | ```` object or ``None`` if ``errors`` is not empty 18 | ``errors`` 19 | a tuple with error messages 20 | ``warnings`` 21 | a list with warnings 22 | ``used_names`` 23 | a dictionary mapping collected used names to ``True``. 24 | 25 | These details can be used to inform the user about the compiled source code. 26 | 27 | Modifying the builtins is straight forward, it is just a dictionary containing the available library elements. 28 | Modification normally means removing elements from existing builtins or adding allowed elements by copying from globals. 29 | 30 | For frameworks it could possibly also be useful to change the handling of specific Python language elements. 31 | For that use case RestrictedPython provides the possibility to pass an own policy. 32 | 33 | A policy is basically a special ``NodeTransformer`` that could be instantiated with three params for ``errors``, ``warnings`` and ``used_names``, it should be a subclass of RestrictedPython.RestrictingNodeTransformer. 34 | 35 | .. testcode:: own_policy 36 | 37 | from RestrictedPython import compile_restricted 38 | from RestrictedPython import RestrictingNodeTransformer 39 | 40 | class OwnRestrictingNodeTransformer(RestrictingNodeTransformer): 41 | pass 42 | 43 | policy_instance = OwnRestrictingNodeTransformer( 44 | errors=[], 45 | warnings=[], 46 | used_names=[] 47 | ) 48 | 49 | All ``compile_restricted*`` methods do have an optional parameter ``policy``, where a specific policy could be provided. 50 | 51 | .. testcode:: own_policy 52 | 53 | source_code = """ 54 | def do_something(): 55 | pass 56 | """ 57 | 58 | policy = OwnRestrictingNodeTransformer 59 | 60 | byte_code = compile_restricted( 61 | source_code, 62 | filename='', 63 | mode='exec', 64 | policy=policy # policy class 65 | ) 66 | exec(byte_code, globals(), None) 67 | 68 | One special case "unrestricted RestrictedPython" (defined to unblock ports of Zope Packages to Python 3) is to actually use RestrictedPython in an unrestricted mode, by providing a Null-Policy (aka ``None``). 69 | That special case would be written as: 70 | 71 | .. testcode:: 72 | 73 | from RestrictedPython import compile_restricted 74 | 75 | source_code = """ 76 | def do_something(): 77 | pass 78 | """ 79 | 80 | byte_code = compile_restricted( 81 | source_code, 82 | filename='', 83 | mode='exec', 84 | policy=None # Null-Policy -> unrestricted 85 | ) 86 | exec(byte_code, globals(), None) 87 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # Generated from: 2 | # https://github.com/zopefoundation/meta/tree/master/config/pure-python 3 | [tox] 4 | minversion = 3.18 5 | envlist = 6 | release-check 7 | lint 8 | py39 9 | py310 10 | py311 11 | py312 12 | py313 13 | py314 14 | docs 15 | coverage 16 | py311-datetime 17 | combined-coverage 18 | 19 | [testenv] 20 | usedevelop = true 21 | package = wheel 22 | wheel_build_env = .pkg 23 | pip_pre = py314: true 24 | deps = 25 | setuptools >= 78.1.1,< 81 26 | datetime: DateTime 27 | -cconstraints.txt 28 | pytest-cov 29 | Sphinx 30 | setenv = 31 | COVERAGE_FILE=.coverage.{envname} 32 | commands = 33 | python -V 34 | pytest --cov=src --cov=tests --cov-report= tests {posargs} 35 | sphinx-build -b doctest -d {envdir}/.cache/doctrees docs {envdir}/.cache/doctest 36 | extras = 37 | test 38 | docs 39 | 40 | [testenv:combined-coverage] 41 | basepython = python3 42 | allowlist_externals = 43 | mkdir 44 | deps = 45 | coverage 46 | -cconstraints.txt 47 | setenv = 48 | COVERAGE_FILE=.coverage 49 | commands = 50 | mkdir -p {toxinidir}/parts/htmlcov 51 | coverage erase 52 | coverage combine 53 | coverage html 54 | coverage report -m --fail-under=100 55 | depends = py39,py310,py311,py311-datetime,py312,py313,py314,coverage 56 | 57 | [testenv:setuptools-latest] 58 | basepython = python3 59 | deps = 60 | git+https://github.com/pypa/setuptools.git\#egg=setuptools 61 | datetime: DateTime 62 | -cconstraints.txt 63 | pytest-cov 64 | 65 | [testenv:release-check] 66 | description = ensure that the distribution is ready to release 67 | basepython = python3 68 | skip_install = true 69 | deps = 70 | setuptools >= 78.1.1,< 81 71 | wheel 72 | twine 73 | build 74 | check-manifest 75 | check-python-versions >= 0.20.0 76 | wheel 77 | commands_pre = 78 | commands = 79 | check-manifest 80 | check-python-versions --only setup.py,tox.ini,.github/workflows/tests.yml 81 | python -m build --sdist --no-isolation 82 | twine check dist/* 83 | 84 | [testenv:lint] 85 | description = This env runs all linters configured in .pre-commit-config.yaml 86 | basepython = python3 87 | skip_install = true 88 | deps = 89 | pre-commit 90 | commands_pre = 91 | commands = 92 | pre-commit run --all-files --show-diff-on-failure 93 | 94 | [testenv:docs] 95 | basepython = python3 96 | skip_install = false 97 | commands_pre = 98 | commands = 99 | sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html 100 | sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/doctest 101 | 102 | [testenv:coverage] 103 | basepython = python3 104 | allowlist_externals = 105 | mkdir 106 | deps = 107 | coverage[toml] 108 | datetime: DateTime 109 | -cconstraints.txt 110 | pytest-cov 111 | Sphinx 112 | setenv = 113 | COVERAGE_FILE=.coverage 114 | commands = 115 | mkdir -p {toxinidir}/parts/htmlcov 116 | pytest --cov=src --cov=tests --cov-report= tests {posargs} 117 | coverage run -a -m sphinx -b doctest -d {envdir}/.cache/doctrees docs {envdir}/.cache/doctest 118 | coverage html 119 | coverage report 120 | -------------------------------------------------------------------------------- /tests/test_eval.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from RestrictedPython.Eval import RestrictionCapableEval 4 | 5 | 6 | exp = """ 7 | {'a':[m.pop()]}['a'] \ 8 | + [m[0]] 9 | """ 10 | 11 | 12 | def test_init(): 13 | ob = RestrictionCapableEval(exp) 14 | 15 | assert ob.expr == "{'a':[m.pop()]}['a'] + [m[0]]" 16 | assert ob.used == ('m', ) 17 | assert ob.ucode is not None 18 | assert ob.rcode is None 19 | 20 | 21 | def test_init_with_syntax_error(): 22 | with pytest.raises(SyntaxError): 23 | RestrictionCapableEval("if:") 24 | 25 | 26 | def test_prepRestrictedCode(): 27 | ob = RestrictionCapableEval(exp) 28 | ob.prepRestrictedCode() 29 | assert ob.used == ('m', ) 30 | assert ob.rcode is not None 31 | 32 | 33 | def test_call(): 34 | ob = RestrictionCapableEval(exp) 35 | ret = ob(m=[1, 2]) 36 | assert ret == [2, 1] 37 | 38 | 39 | def test_eval(): 40 | ob = RestrictionCapableEval(exp) 41 | ret = ob.eval({'m': [1, 2]}) 42 | assert ret == [2, 1] 43 | 44 | 45 | def test_Eval__RestrictionCapableEval_1(): 46 | """It raises SyntaxError when there are errors 47 | (by using forbidden stuff) in the code.""" 48 | ob = RestrictionCapableEval("_a") 49 | with pytest.raises(SyntaxError): 50 | ob.prepRestrictedCode() 51 | 52 | 53 | def test_Eval__RestrictionCapableEval__2(): 54 | """It stores used names.""" 55 | ob = RestrictionCapableEval("[x for x in (1, 2, 3)]") 56 | assert ob.used == ('x',) 57 | 58 | 59 | def test_Eval__RestictionCapableEval__prepUnrestrictedCode_1(): 60 | """It does nothing when unrestricted code is already set by init.""" 61 | ob = RestrictionCapableEval("a") 62 | assert ob.used == ('a',) 63 | ob.expr = "b" 64 | ob.prepUnrestrictedCode() 65 | assert ob.used == ('a',) 66 | 67 | 68 | def test_Eval__RestictionCapableEval__prepUnrestrictedCode_2(): 69 | """It does not re-set 'used' if it is already set by an earlier call.""" 70 | ob = RestrictionCapableEval("a") 71 | assert ob.used == ('a',) 72 | ob.used = ('b',) 73 | # This is needed to force re-compilation 74 | ob.ucode = None 75 | ob.prepUnrestrictedCode() 76 | # If it was called again, used would be ('a',) again. 77 | assert ob.used == ('b',) 78 | 79 | 80 | def test_Eval__RestictionCapableEval__prepRestrictedCode_1(): 81 | """It does nothing when restricted code is already set by 82 | prepRestrictedCode.""" 83 | ob = RestrictionCapableEval("a") 84 | ob.prepRestrictedCode() 85 | assert ob.used == ('a',) 86 | ob.expr = "b" 87 | ob.prepRestrictedCode() 88 | assert ob.used == ('a',) 89 | 90 | 91 | def test_Eval__RestictionCapableEval__eval_1(): 92 | """It does not add names from the mapping to the 93 | global scope which are already there.""" 94 | ob = RestrictionCapableEval("a + b + c") 95 | ob.globals['c'] = 8 96 | result = ob.eval(dict(a=1, b=2, c=4)) 97 | assert result == 11 98 | 99 | 100 | def test_Eval__RestictionCapableEval__eval__2(): 101 | """It allows to use list comprehensions.""" 102 | ob = RestrictionCapableEval("[item for item in (1, 2)]") 103 | result = ob.eval({}) 104 | assert result == [1, 2] 105 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. image:: https://github.com/zopefoundation/RestrictedPython/actions/workflows/tests.yml/badge.svg 2 | :target: https://github.com/zopefoundation/RestrictedPython/actions/workflows/tests.yml 3 | 4 | .. image:: https://coveralls.io/repos/github/zopefoundation/RestrictedPython/badge.svg?branch=master 5 | :target: https://coveralls.io/github/zopefoundation/RestrictedPython?branch=master 6 | 7 | .. image:: https://readthedocs.org/projects/restrictedpython/badge/ 8 | :target: https://restrictedpython.readthedocs.org/ 9 | :alt: Documentation Status 10 | 11 | .. image:: https://img.shields.io/pypi/v/RestrictedPython.svg 12 | :target: https://pypi.org/project/RestrictedPython/ 13 | :alt: Current version on PyPI 14 | 15 | .. image:: https://img.shields.io/pypi/pyversions/RestrictedPython.svg 16 | :target: https://pypi.org/project/RestrictedPython/ 17 | :alt: Supported Python versions 18 | 19 | .. image:: https://github.com/zopefoundation/RestrictedPython/raw/master/docs/logo.jpg 20 | 21 | ================ 22 | RestrictedPython 23 | ================ 24 | 25 | RestrictedPython is a tool that helps to define a subset of the Python language which allows to provide a program input into a trusted environment. 26 | RestrictedPython is not a sandbox system or a secured environment, but it helps to define a trusted environment and execute untrusted code inside of it. 27 | 28 | .. warning:: 29 | 30 | RestrictedPython only supports CPython. It does _not_ support PyPy and other Python implementations as it cannot provide its restrictions there. 31 | 32 | For full documentation please see http://restrictedpython.readthedocs.io/. 33 | 34 | Example 35 | ======= 36 | 37 | To give a basic understanding what RestrictedPython does here two examples: 38 | 39 | An unproblematic code example 40 | ----------------------------- 41 | 42 | Python allows you to execute a large set of commands. 43 | This would not harm any system. 44 | 45 | .. code-block:: pycon 46 | 47 | >>> from RestrictedPython import compile_restricted 48 | >>> from RestrictedPython import safe_globals 49 | >>> 50 | >>> source_code = """ 51 | ... def example(): 52 | ... return 'Hello World!' 53 | ... """ 54 | >>> 55 | >>> loc = {} 56 | >>> byte_code = compile_restricted(source_code, '', 'exec') 57 | >>> exec(byte_code, safe_globals, loc) 58 | >>> 59 | >>> loc['example']() 60 | 'Hello World!' 61 | 62 | Problematic code example 63 | ------------------------ 64 | 65 | This example directly executed in Python could harm your system. 66 | 67 | .. code-block:: pycon 68 | 69 | >>> from RestrictedPython import compile_restricted 70 | >>> from RestrictedPython import safe_globals 71 | >>> 72 | >>> source_code = """ 73 | ... import os 74 | ... 75 | ... os.listdir('/') 76 | ... """ 77 | >>> byte_code = compile_restricted(source_code, '', 'exec') 78 | >>> exec(byte_code, safe_globals, {}) 79 | Traceback (most recent call last): 80 | ImportError: __import__ not found 81 | 82 | Contributing to RestrictedPython 83 | -------------------------------- 84 | 85 | If you want to help maintain RestrictedPython and contribute, please refer to 86 | the documentation `Contributing page 87 | `_. 88 | -------------------------------------------------------------------------------- /src/RestrictedPython/Utilities.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2002 Zope Foundation and Contributors. 4 | # 5 | # This software is subject to the provisions of the Zope Public License, 6 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 7 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 8 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 9 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 10 | # FOR A PARTICULAR PURPOSE 11 | # 12 | ############################################################################## 13 | 14 | import math 15 | import random 16 | import string 17 | 18 | 19 | utility_builtins = {} 20 | 21 | 22 | class _AttributeDelegator: 23 | def __init__(self, mod, *excludes): 24 | """delegate attribute lookups outside *excludes* to module *mod*.""" 25 | self.__mod = mod 26 | self.__excludes = excludes 27 | 28 | def __getattr__(self, attr): 29 | if attr in self.__excludes: 30 | raise NotImplementedError( 31 | f"{self.__mod.__name__}.{attr} is not safe") 32 | try: 33 | return getattr(self.__mod, attr) 34 | except AttributeError as e: 35 | e.obj = self 36 | raise 37 | 38 | 39 | utility_builtins['string'] = _AttributeDelegator(string, "Formatter") 40 | utility_builtins['math'] = math 41 | utility_builtins['random'] = random 42 | utility_builtins['whrandom'] = random 43 | utility_builtins['set'] = set 44 | utility_builtins['frozenset'] = frozenset 45 | 46 | try: 47 | import DateTime 48 | utility_builtins['DateTime'] = DateTime.DateTime 49 | except ImportError: 50 | pass 51 | 52 | 53 | def same_type(arg1, *args): 54 | """Compares the class or type of two or more objects.""" 55 | t = getattr(arg1, '__class__', type(arg1)) 56 | for arg in args: 57 | if getattr(arg, '__class__', type(arg)) is not t: 58 | return False 59 | return True 60 | 61 | 62 | utility_builtins['same_type'] = same_type 63 | 64 | 65 | def test(*args): 66 | length = len(args) 67 | for i in range(1, length, 2): 68 | if args[i - 1]: 69 | return args[i] 70 | 71 | if length % 2: 72 | return args[-1] 73 | 74 | 75 | utility_builtins['test'] = test 76 | 77 | 78 | def reorder(s, with_=None, without=()): 79 | # s, with_, and without are sequences treated as sets. 80 | # The result is subtract(intersect(s, with_), without), 81 | # unless with_ is None, in which case it is subtract(s, without). 82 | if with_ is None: 83 | with_ = s 84 | orig = {} 85 | for item in s: 86 | if isinstance(item, tuple) and len(item) == 2: 87 | key, value = item 88 | else: 89 | key = value = item 90 | orig[key] = value 91 | 92 | result = [] 93 | 94 | for item in without: 95 | if isinstance(item, tuple) and len(item) == 2: 96 | key, ignored = item 97 | else: 98 | key = item 99 | if key in orig: 100 | del orig[key] 101 | 102 | for item in with_: 103 | if isinstance(item, tuple) and len(item) == 2: 104 | key, ignored = item 105 | else: 106 | key = item 107 | if key in orig: 108 | result.append((key, orig[key])) 109 | del orig[key] 110 | 111 | return result 112 | 113 | 114 | utility_builtins['reorder'] = reorder 115 | -------------------------------------------------------------------------------- /src/RestrictedPython/Eval.py: -------------------------------------------------------------------------------- 1 | ############################################################################## 2 | # 3 | # Copyright (c) 2002 Zope Foundation and Contributors. 4 | # 5 | # This software is subject to the provisions of the Zope Public License, 6 | # Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. 7 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED 8 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 9 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS 10 | # FOR A PARTICULAR PURPOSE 11 | # 12 | ############################################################################## 13 | """Restricted Python Expressions.""" 14 | 15 | import ast 16 | 17 | from .compile import compile_restricted_eval 18 | 19 | 20 | nltosp = str.maketrans('\r\n', ' ') 21 | 22 | # No restrictions. 23 | default_guarded_getattr = getattr 24 | 25 | 26 | def default_guarded_getitem(ob, index): 27 | # No restrictions. 28 | return ob[index] 29 | 30 | 31 | def default_guarded_getiter(ob): 32 | # No restrictions. 33 | return ob 34 | 35 | 36 | class RestrictionCapableEval: 37 | """A base class for restricted code.""" 38 | 39 | globals = {'__builtins__': None} 40 | # restricted 41 | rcode = None 42 | 43 | # unrestricted 44 | ucode = None 45 | 46 | # Names used by the expression 47 | used = None 48 | 49 | def __init__(self, expr): 50 | """Create a restricted expression 51 | 52 | where: 53 | 54 | expr -- a string containing the expression to be evaluated. 55 | """ 56 | expr = expr.strip() 57 | self.__name__ = expr 58 | expr = expr.translate(nltosp) 59 | self.expr = expr 60 | # Catch syntax errors. 61 | self.prepUnrestrictedCode() 62 | 63 | def prepRestrictedCode(self): 64 | if self.rcode is None: 65 | result = compile_restricted_eval(self.expr, '') 66 | if result.errors: 67 | raise SyntaxError(result.errors[0]) 68 | self.used = tuple(result.used_names) 69 | self.rcode = result.code 70 | 71 | def prepUnrestrictedCode(self): 72 | if self.ucode is None: 73 | exp_node = compile( 74 | self.expr, 75 | '', 76 | 'eval', 77 | ast.PyCF_ONLY_AST) 78 | 79 | co = compile(exp_node, '', 'eval') 80 | 81 | # Examine the ast to discover which names the expression needs. 82 | if self.used is None: 83 | used = set() 84 | for node in ast.walk(exp_node): 85 | if isinstance(node, ast.Name): 86 | if isinstance(node.ctx, ast.Load): 87 | used.add(node.id) 88 | 89 | self.used = tuple(used) 90 | 91 | self.ucode = co 92 | 93 | def eval(self, mapping): 94 | # This default implementation is probably not very useful. :-( 95 | # This is meant to be overridden. 96 | self.prepRestrictedCode() 97 | 98 | global_scope = { 99 | '_getattr_': default_guarded_getattr, 100 | '_getitem_': default_guarded_getitem, 101 | '_getiter_': default_guarded_getiter, 102 | } 103 | 104 | global_scope.update(self.globals) 105 | 106 | for name in self.used: 107 | if (name not in global_scope) and (name in mapping): 108 | global_scope[name] = mapping[name] 109 | 110 | return eval(self.rcode, global_scope) 111 | 112 | def __call__(self, **kw): 113 | return self.eval(kw) 114 | -------------------------------------------------------------------------------- /tests/transformer/test_call.py: -------------------------------------------------------------------------------- 1 | from RestrictedPython import compile_restricted_exec 2 | from tests.helper import restricted_exec 3 | 4 | 5 | def test_RestrictingNodeTransformer__visit_Call__1(): 6 | """It compiles a function call successfully and returns the used name.""" 7 | result = compile_restricted_exec('a = max([1, 2, 3])') 8 | assert result.errors == () 9 | loc = {} 10 | exec(result.code, {}, loc) 11 | assert loc['a'] == 3 12 | assert result.used_names == {'max': True} 13 | 14 | 15 | # def f(a, b, c): pass 16 | # f(*two_element_sequence, **dict_with_key_c) 17 | # 18 | # makes the elements of two_element_sequence 19 | # visible to f via its 'a' and 'b' arguments, 20 | # and the dict_with_key_c['c'] value visible via its 'c' argument. 21 | # It is a devious way to extract values without going through security checks. 22 | 23 | FUNCTIONC_CALLS = """ 24 | star = (3, 4) 25 | kwargs = {'x': 5, 'y': 6} 26 | 27 | def positional_args(): 28 | return foo(1, 2) 29 | 30 | def star_args(): 31 | return foo(*star) 32 | 33 | def positional_and_star_args(): 34 | return foo(1, 2, *star) 35 | 36 | def kw_args(): 37 | return foo(**kwargs) 38 | 39 | def star_and_kw(): 40 | return foo(*star, **kwargs) 41 | 42 | def positional_and_star_and_kw_args(): 43 | return foo(1, *star, **kwargs) 44 | 45 | def positional_and_star_and_keyword_and_kw_args(): 46 | return foo(1, 2, *star, r=9, **kwargs) 47 | """ 48 | 49 | 50 | def test_RestrictingNodeTransformer__visit_Call__2(mocker): 51 | _apply_ = mocker.stub() 52 | _apply_.side_effect = lambda func, *args, **kwargs: func(*args, **kwargs) 53 | 54 | glb = { 55 | '_apply_': _apply_, 56 | 'foo': lambda *args, **kwargs: (args, kwargs) 57 | } 58 | 59 | restricted_exec(FUNCTIONC_CALLS, glb) 60 | 61 | ret = glb['positional_args']() 62 | assert ((1, 2), {}) == ret 63 | assert _apply_.called is False 64 | _apply_.reset_mock() 65 | 66 | ret = glb['star_args']() 67 | ref = ((3, 4), {}) 68 | assert ref == ret 69 | _apply_.assert_called_once_with(glb['foo'], *ref[0]) 70 | _apply_.reset_mock() 71 | 72 | ret = glb['positional_and_star_args']() 73 | ref = ((1, 2, 3, 4), {}) 74 | assert ref == ret 75 | _apply_.assert_called_once_with(glb['foo'], *ref[0]) 76 | _apply_.reset_mock() 77 | 78 | ret = glb['kw_args']() 79 | ref = ((), {'x': 5, 'y': 6}) 80 | assert ref == ret 81 | _apply_.assert_called_once_with(glb['foo'], **ref[1]) 82 | _apply_.reset_mock() 83 | 84 | ret = glb['star_and_kw']() 85 | ref = ((3, 4), {'x': 5, 'y': 6}) 86 | assert ref == ret 87 | _apply_.assert_called_once_with(glb['foo'], *ref[0], **ref[1]) 88 | _apply_.reset_mock() 89 | 90 | ret = glb['positional_and_star_and_kw_args']() 91 | ref = ((1, 3, 4), {'x': 5, 'y': 6}) 92 | assert ref == ret 93 | _apply_.assert_called_once_with(glb['foo'], *ref[0], **ref[1]) 94 | _apply_.reset_mock() 95 | 96 | ret = glb['positional_and_star_and_keyword_and_kw_args']() 97 | ref = ((1, 2, 3, 4), {'x': 5, 'y': 6, 'r': 9}) 98 | assert ref == ret 99 | _apply_.assert_called_once_with(glb['foo'], *ref[0], **ref[1]) 100 | _apply_.reset_mock() 101 | 102 | 103 | def test_visit_Call__private_function(): 104 | """Calling private functions is forbidden.""" 105 | result = compile_restricted_exec('__init__(1)') 106 | assert result.errors == ( 107 | 'Line 1: "__init__" is an invalid variable name because it starts with "_"', # NOQA: E501 108 | ) 109 | 110 | 111 | def test_visit_Call__private_method(): 112 | """Calling private methods is forbidden.""" 113 | result = compile_restricted_exec('Int.__init__(1)') 114 | assert result.errors == ( 115 | 'Line 1: "__init__" is an invalid attribute name because it starts with "_".', # NOQA: E501 116 | ) 117 | -------------------------------------------------------------------------------- /docs/idea.rst: -------------------------------------------------------------------------------- 1 | The idea behind RestrictedPython 2 | ================================ 3 | 4 | Python is a `Turing complete `_ programming language. 5 | To offer a Python interface for users in web context is a potential security risk. 6 | Web frameworks and Content Management Systems (CMS) want to offer their users as much extensibility as possible through the web (TTW). 7 | This also means to have permissions to add functionality via a Python script. 8 | 9 | There should be additional preventive measures taken to ensure integrity of the application and the server itself, according to information security best practice and unrelated to RestrictedPython. 10 | 11 | RestrictedPython defines a safe subset of the Python programming language. 12 | This is a common approach for securing a programming language. 13 | The `Ada Ravenscar profile `_ is another example of such an approach. 14 | 15 | Defining a secure subset of the language involves restricting the `EBNF `_ elements and explicitly allowing or disallowing language features. 16 | Much of the power of a programming language derives from its standard and contributed libraries, so any calling of these methods must also be checked and potentially restricted. 17 | RestrictedPython generally disallows calls to any library that is not explicit whitelisted. 18 | 19 | As Python is a scripting language that is executed by an interpreter any Python code that should be executed has to be explicitly checked before executing the generated byte code by the interpreter. 20 | 21 | Python itself offers three methods that provide such a workflow: 22 | 23 | * ``compile()`` which compiles source code to byte code 24 | * ``exec`` / ``exec()`` which executes the byte code in the interpreter 25 | * ``eval`` / ``eval()`` which executes a byte code expression 26 | 27 | Therefore RestrictedPython offers a replacement for the python builtin function ``compile()`` (`Python 2 `_ / `Python 3 `_). 28 | This Python function is defined as following: 29 | 30 | .. code-block:: python 31 | 32 | compile(source, filename, mode [, flags [, dont_inherit]]) 33 | 34 | The definition of the ``compile()`` method has changed over time, but its relevant parameters ``source`` and ``mode`` still remain. 35 | 36 | There are three valid string values for ``mode``: 37 | 38 | * ``'exec'`` 39 | * ``'eval'`` 40 | * ``'single'`` 41 | 42 | For RestrictedPython this ``compile()`` method is replaced by: 43 | 44 | .. code-block:: python 45 | 46 | RestrictedPython.compile_restricted(source, filename, mode [, flags [, dont_inherit]]) 47 | 48 | The primary parameter ``source`` has to be a string or ``ast.AST`` instance. 49 | Both methods either return compiled byte code that the interpreter can execute or raise exceptions if the provided source code is invalid. 50 | 51 | As ``compile`` and ``compile_restricted`` just compile the provided source code to byte code it is not sufficient as a sandbox environment, as all calls to libraries are still available. 52 | 53 | The two methods / statements: 54 | 55 | * ``exec`` / ``exec()`` 56 | * ``eval`` / ``eval()`` 57 | 58 | have two parameters: 59 | 60 | * ``globals`` 61 | * ``locals`` 62 | 63 | which are references to the Python builtins. 64 | 65 | By modifying and restricting the available modules, methods and constants from ``globals`` and ``locals`` we can limit the possible calls. 66 | 67 | Additionally RestrictedPython offers a way to define a policy which allows developers to protect access to attributes. 68 | This works by defining a restricted version of: 69 | 70 | * ``print`` 71 | * ``getattr`` 72 | * ``setattr`` 73 | * ``import`` 74 | 75 | Also RestrictedPython provides three predefined, limited versions of Python's ``__builtins__``: 76 | 77 | * ``safe_builtins`` (by Guards.py) 78 | * ``limited_builtins`` (by Limits.py), which provides restricted sequence types 79 | * ``utilities_builtins`` (by Utilities.py), which provides access for standard modules math, random, string and for sets. 80 | 81 | One special shortcut: 82 | 83 | * ``safe_globals`` for ``{'__builtins__': safe_builtins}`` (by Guards.py) 84 | 85 | Additional there exist guard functions to make attributes of Python objects immutable --> ``full_write_guard`` (write and delete protected). 86 | -------------------------------------------------------------------------------- /tests/transformer/test_attribute.py: -------------------------------------------------------------------------------- 1 | from RestrictedPython import compile_restricted_exec 2 | from tests.helper import restricted_exec 3 | 4 | 5 | BAD_ATTR_UNDERSCORE = """\ 6 | def bad_attr(): 7 | some_ob = object() 8 | some_ob._some_attr = 15 9 | """ 10 | 11 | 12 | def test_RestrictingNodeTransformer__visit_Attribute__1(): 13 | """It is an error if a bad attribute name is used.""" 14 | result = compile_restricted_exec(BAD_ATTR_UNDERSCORE) 15 | assert result.errors == ( 16 | 'Line 3: "_some_attr" is an invalid attribute name because it ' 17 | 'starts with "_".',) 18 | 19 | 20 | BAD_ATTR_ROLES = """\ 21 | def bad_attr(): 22 | some_ob = object() 23 | some_ob.abc__roles__ 24 | """ 25 | 26 | 27 | def test_RestrictingNodeTransformer__visit_Attribute__2(): 28 | """It is an error if a bad attribute name is used.""" 29 | result = compile_restricted_exec(BAD_ATTR_ROLES) 30 | assert result.errors == ( 31 | 'Line 3: "abc__roles__" is an invalid attribute name because it ' 32 | 'ends with "__roles__".',) 33 | 34 | 35 | TRANSFORM_ATTRIBUTE_ACCESS = """\ 36 | def func(): 37 | return a.b 38 | """ 39 | 40 | 41 | def test_RestrictingNodeTransformer__visit_Attribute__3(mocker): 42 | """It transforms the attribute access to `_getattr_`.""" 43 | glb = { 44 | '_getattr_': mocker.stub(), 45 | 'a': [], 46 | 'b': 'b' 47 | } 48 | restricted_exec(TRANSFORM_ATTRIBUTE_ACCESS, glb) 49 | glb['func']() 50 | glb['_getattr_'].assert_called_once_with([], 'b') 51 | 52 | 53 | ALLOW_UNDERSCORE_ONLY = """\ 54 | def func(): 55 | some_ob = object() 56 | some_ob._ 57 | """ 58 | 59 | 60 | def test_RestrictingNodeTransformer__visit_Attribute__4(): 61 | """It allows `_` as attribute name.""" 62 | result = compile_restricted_exec(ALLOW_UNDERSCORE_ONLY) 63 | assert result.errors == () 64 | 65 | 66 | def test_RestrictingNodeTransformer__visit_Attribute__5( 67 | mocker): 68 | """It transforms writing to an attribute to `_write_`.""" 69 | glb = { 70 | '_write_': mocker.stub(), 71 | 'a': mocker.stub(), 72 | } 73 | glb['_write_'].return_value = glb['a'] 74 | 75 | restricted_exec("a.b = 'it works'", glb) 76 | 77 | glb['_write_'].assert_called_once_with(glb['a']) 78 | assert glb['a'].b == 'it works' 79 | 80 | 81 | def test_RestrictingNodeTransformer__visit_Attribute__5_5( 82 | mocker): 83 | """It transforms deleting of an attribute to `_write_`.""" 84 | glb = { 85 | '_write_': mocker.stub(), 86 | 'a': mocker.stub(), 87 | } 88 | glb['a'].b = 'it exists' 89 | glb['_write_'].return_value = glb['a'] 90 | 91 | restricted_exec("del a.b", glb) 92 | 93 | glb['_write_'].assert_called_once_with(glb['a']) 94 | assert not hasattr(glb['a'], 'b') 95 | 96 | 97 | DISALLOW_TRACEBACK_ACCESS = """ 98 | try: 99 | raise Exception() 100 | except Exception as e: 101 | tb = e.__traceback__ 102 | """ 103 | 104 | 105 | def test_RestrictingNodeTransformer__visit_Attribute__6(): 106 | """It denies access to the __traceback__ attribute.""" 107 | result = compile_restricted_exec(DISALLOW_TRACEBACK_ACCESS) 108 | assert result.errors == ( 109 | 'Line 5: "__traceback__" is an invalid attribute name because ' 110 | 'it starts with "_".',) 111 | 112 | 113 | TRANSFORM_ATTRIBUTE_ACCESS_FUNCTION_DEFAULT = """ 114 | def func_default(x=a.a): 115 | return x 116 | """ 117 | 118 | 119 | def test_RestrictingNodeTransformer__visit_Attribute__7( 120 | mocker): 121 | """It transforms attribute access in function default kw to `_write_`.""" 122 | _getattr_ = mocker.Mock() 123 | _getattr_.side_effect = getattr 124 | 125 | glb = { 126 | '_getattr_': _getattr_, 127 | 'a': mocker.Mock(a=1), 128 | } 129 | 130 | restricted_exec(TRANSFORM_ATTRIBUTE_ACCESS_FUNCTION_DEFAULT, glb) 131 | 132 | _getattr_.assert_has_calls([mocker.call(glb['a'], 'a')]) 133 | assert glb['func_default']() == 1 134 | 135 | 136 | def test_RestrictingNodeTransformer__visit_Attribute__8( 137 | mocker): 138 | """It transforms attribute access in lamda default kw to `_write_`.""" 139 | _getattr_ = mocker.Mock() 140 | _getattr_.side_effect = getattr 141 | 142 | glb = { 143 | '_getattr_': _getattr_, 144 | 'b': mocker.Mock(b=2) 145 | } 146 | 147 | restricted_exec('lambda_default = lambda x=b.b: x', glb) 148 | 149 | _getattr_.assert_has_calls([mocker.call(glb['b'], 'b')]) 150 | assert glb['lambda_default']() == 2 151 | -------------------------------------------------------------------------------- /tests/transformer/test_iterator.py: -------------------------------------------------------------------------------- 1 | import types 2 | 3 | from RestrictedPython.Guards import guarded_iter_unpack_sequence 4 | from tests.helper import restricted_exec 5 | 6 | 7 | ITERATORS = """ 8 | def for_loop(it): 9 | c = 0 10 | for a in it: 11 | c = c + a 12 | return c 13 | 14 | 15 | def nested_for_loop(it1, it2): 16 | c = 0 17 | for a in it1: 18 | for b in it2: 19 | c = c + a + b 20 | return c 21 | 22 | def dict_comp(it): 23 | return {a: a + a for a in it} 24 | 25 | def list_comp(it): 26 | return [a + a for a in it] 27 | 28 | def nested_list_comp(it1, it2): 29 | return [a + b for a in it1 if a > 1 for b in it2] 30 | 31 | def set_comp(it): 32 | return {a + a for a in it} 33 | 34 | def generator(it): 35 | return (a + a for a in it) 36 | 37 | def nested_generator(it1, it2): 38 | return (a+b for a in it1 if a > 0 for b in it2) 39 | """ 40 | 41 | 42 | def test_RestrictingNodeTransformer__guard_iter__1(mocker): 43 | it = (1, 2, 3) 44 | _getiter_ = mocker.stub() 45 | _getiter_.side_effect = lambda x: x 46 | glb = {'_getiter_': _getiter_} 47 | restricted_exec(ITERATORS, glb) 48 | 49 | ret = glb['for_loop'](it) 50 | assert 6 == ret 51 | _getiter_.assert_called_once_with(it) 52 | _getiter_.reset_mock() 53 | 54 | ret = glb['nested_for_loop']((1, 2), (3, 4)) 55 | assert 20 == ret 56 | _getiter_.assert_has_calls([ 57 | mocker.call((1, 2)), 58 | mocker.call((3, 4)) 59 | ]) 60 | _getiter_.reset_mock() 61 | 62 | ret = glb['dict_comp'](it) 63 | assert {1: 2, 2: 4, 3: 6} == ret 64 | _getiter_.assert_called_once_with(it) 65 | _getiter_.reset_mock() 66 | 67 | ret = glb['list_comp'](it) 68 | assert [2, 4, 6] == ret 69 | _getiter_.assert_called_once_with(it) 70 | _getiter_.reset_mock() 71 | 72 | ret = glb['nested_list_comp']((1, 2), (3, 4)) 73 | assert [5, 6] == ret 74 | _getiter_.assert_has_calls([ 75 | mocker.call((1, 2)), 76 | mocker.call((3, 4)) 77 | ]) 78 | _getiter_.reset_mock() 79 | 80 | ret = glb['set_comp'](it) 81 | assert {2, 4, 6} == ret 82 | _getiter_.assert_called_once_with(it) 83 | _getiter_.reset_mock() 84 | 85 | ret = glb['generator'](it) 86 | assert isinstance(ret, types.GeneratorType) 87 | assert list(ret) == [2, 4, 6] 88 | _getiter_.assert_called_once_with(it) 89 | _getiter_.reset_mock() 90 | 91 | ret = glb['nested_generator']((0, 1, 2), (1, 2)) 92 | assert isinstance(ret, types.GeneratorType) 93 | assert list(ret) == [2, 3, 3, 4] 94 | _getiter_.assert_has_calls([ 95 | mocker.call((0, 1, 2)), 96 | mocker.call((1, 2)), 97 | mocker.call((1, 2))]) 98 | _getiter_.reset_mock() 99 | 100 | 101 | ITERATORS_WITH_UNPACK_SEQUENCE = """ 102 | def for_loop(it): 103 | c = 0 104 | for (a, b) in it: 105 | c = c + a + b 106 | return c 107 | 108 | def dict_comp(it): 109 | return {a: a + b for (a, b) in it} 110 | 111 | def list_comp(it): 112 | return [a + b for (a, b) in it] 113 | 114 | def set_comp(it): 115 | return {a + b for (a, b) in it} 116 | 117 | def generator(it): 118 | return (a + b for (a, b) in it) 119 | """ 120 | 121 | 122 | def test_RestrictingNodeTransformer__guard_iter__2(mocker): 123 | it = ((1, 2), (3, 4), (5, 6)) 124 | 125 | call_ref = [ 126 | mocker.call(it), 127 | mocker.call(it[0]), 128 | mocker.call(it[1]), 129 | mocker.call(it[2]) 130 | ] 131 | 132 | _getiter_ = mocker.stub() 133 | _getiter_.side_effect = lambda x: x 134 | 135 | glb = { 136 | '_getiter_': _getiter_, 137 | '_iter_unpack_sequence_': guarded_iter_unpack_sequence 138 | } 139 | 140 | restricted_exec(ITERATORS_WITH_UNPACK_SEQUENCE, glb) 141 | 142 | ret = glb['for_loop'](it) 143 | assert ret == 21 144 | _getiter_.assert_has_calls(call_ref) 145 | _getiter_.reset_mock() 146 | 147 | ret = glb['dict_comp'](it) 148 | assert ret == {1: 3, 3: 7, 5: 11} 149 | _getiter_.assert_has_calls(call_ref) 150 | _getiter_.reset_mock() 151 | 152 | ret = glb['list_comp'](it) 153 | assert ret == [3, 7, 11] 154 | _getiter_.assert_has_calls(call_ref) 155 | _getiter_.reset_mock() 156 | 157 | ret = glb['set_comp'](it) 158 | assert ret == {3, 7, 11} 159 | _getiter_.assert_has_calls(call_ref) 160 | _getiter_.reset_mock() 161 | 162 | ret = list(glb['generator'](it)) 163 | assert ret == [3, 7, 11] 164 | _getiter_.assert_has_calls(call_ref) 165 | _getiter_.reset_mock() 166 | -------------------------------------------------------------------------------- /tests/transformer/test_try.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from RestrictedPython import compile_restricted_exec 4 | from RestrictedPython._compat import IS_PY311_OR_GREATER 5 | from tests.helper import restricted_exec 6 | 7 | 8 | TRY_EXCEPT = """ 9 | def try_except(m): 10 | try: 11 | m('try') 12 | raise IndentationError('f1') 13 | except IndentationError as error: 14 | m('except') 15 | """ 16 | 17 | 18 | def test_RestrictingNodeTransformer__visit_Try__1( 19 | mocker): 20 | """It allows try-except statements.""" 21 | trace = mocker.stub() 22 | restricted_exec(TRY_EXCEPT)['try_except'](trace) 23 | 24 | trace.assert_has_calls([ 25 | mocker.call('try'), 26 | mocker.call('except') 27 | ]) 28 | 29 | 30 | TRY_EXCEPT_ELSE = """ 31 | def try_except_else(m): 32 | try: 33 | m('try') 34 | except: 35 | m('except') 36 | else: 37 | m('else') 38 | """ 39 | 40 | 41 | def test_RestrictingNodeTransformer__visit_Try__2( 42 | mocker): 43 | """It allows try-except-else statements.""" 44 | trace = mocker.stub() 45 | restricted_exec(TRY_EXCEPT_ELSE)['try_except_else'](trace) 46 | 47 | trace.assert_has_calls([ 48 | mocker.call('try'), 49 | mocker.call('else') 50 | ]) 51 | 52 | 53 | TRY_EXCEPT_STAR = """ 54 | def try_except_star(m): 55 | try: 56 | m('try') 57 | raise ExceptionGroup("group", [IndentationError('f1'), ValueError(65)]) 58 | except* IndentationError: 59 | m('IndentationError') 60 | except* ValueError: 61 | m('ValueError') 62 | except* RuntimeError: 63 | m('RuntimeError') 64 | """ 65 | 66 | 67 | @pytest.mark.skipif( 68 | not IS_PY311_OR_GREATER, 69 | reason="ExceptionGroup class was added in Python 3.11.", 70 | ) 71 | def test_RestrictingNodeTransformer__visit_TryStar__1(): 72 | """It denies try-except* PEP 654 statements.""" 73 | result = compile_restricted_exec(TRY_EXCEPT_STAR) 74 | assert result.errors == ( 75 | 'Line 3: TryStar statements are not allowed.', 76 | ) 77 | assert result.code is None 78 | 79 | 80 | TRY_FINALLY = """ 81 | def try_finally(m): 82 | try: 83 | m('try') 84 | 1 / 0 85 | finally: 86 | m('finally') 87 | return 88 | """ 89 | 90 | 91 | def test_RestrictingNodeTransformer__visit_TryFinally__1( 92 | mocker): 93 | """It allows try-finally statements.""" 94 | trace = mocker.stub() 95 | restricted_exec(TRY_FINALLY)['try_finally'](trace) 96 | 97 | trace.assert_has_calls([ 98 | mocker.call('try'), 99 | mocker.call('finally') 100 | ]) 101 | 102 | 103 | TRY_EXCEPT_FINALLY = """ 104 | def try_except_finally(m): 105 | try: 106 | m('try') 107 | 1 / 0 108 | except: 109 | m('except') 110 | finally: 111 | m('finally') 112 | """ 113 | 114 | 115 | def test_RestrictingNodeTransformer__visit_TryFinally__2( 116 | mocker): 117 | """It allows try-except-finally statements.""" 118 | trace = mocker.stub() 119 | restricted_exec(TRY_EXCEPT_FINALLY)['try_except_finally'](trace) 120 | 121 | trace.assert_has_calls([ 122 | mocker.call('try'), 123 | mocker.call('except'), 124 | mocker.call('finally') 125 | ]) 126 | 127 | 128 | TRY_EXCEPT_ELSE_FINALLY = """ 129 | def try_except_else_finally(m): 130 | try: 131 | m('try') 132 | except: 133 | m('except') 134 | else: 135 | m('else') 136 | finally: 137 | m('finally') 138 | """ 139 | 140 | 141 | def test_RestrictingNodeTransformer__visit_TryFinally__3( 142 | mocker): 143 | """It allows try-except-else-finally statements.""" 144 | trace = mocker.stub() 145 | restricted_exec(TRY_EXCEPT_ELSE_FINALLY)['try_except_else_finally'](trace) 146 | 147 | trace.assert_has_calls([ 148 | mocker.call('try'), 149 | mocker.call('else'), 150 | mocker.call('finally') 151 | ]) 152 | 153 | 154 | BAD_TRY_EXCEPT = """ 155 | def except_using_bad_name(): 156 | try: 157 | foo 158 | except NameError as _leading_underscore: 159 | # The name of choice (say, _write) is now assigned to an exception 160 | # object. Hard to exploit, but conceivable. 161 | pass 162 | """ 163 | 164 | 165 | def test_RestrictingNodeTransformer__visit_ExceptHandler__2(): 166 | """It denies bad names in the except as statement.""" 167 | result = compile_restricted_exec(BAD_TRY_EXCEPT) 168 | assert result.errors == ( 169 | 'Line 5: "_leading_underscore" is an invalid variable name because ' 170 | 'it starts with "_"',) 171 | -------------------------------------------------------------------------------- /tests/transformer/test_with_stmt.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | 3 | from RestrictedPython import compile_restricted_exec 4 | from RestrictedPython.Guards import guarded_unpack_sequence 5 | from tests.helper import restricted_exec 6 | 7 | 8 | WITH_STMT_WITH_UNPACK_SEQUENCE = """ 9 | def call(ctx): 10 | with ctx() as (a, (c, b)): 11 | return a, c, b 12 | """ 13 | 14 | 15 | def test_with_stmt_unpack_sequence(mocker): 16 | @contextlib.contextmanager 17 | def ctx(): 18 | yield (1, (2, 3)) 19 | 20 | _getiter_ = mocker.stub() 21 | _getiter_.side_effect = lambda ob: ob 22 | 23 | glb = { 24 | '_getiter_': _getiter_, 25 | '_unpack_sequence_': guarded_unpack_sequence 26 | } 27 | 28 | restricted_exec(WITH_STMT_WITH_UNPACK_SEQUENCE, glb) 29 | 30 | ret = glb['call'](ctx) 31 | 32 | assert ret == (1, 2, 3) 33 | _getiter_.assert_has_calls([ 34 | mocker.call((1, (2, 3))), 35 | mocker.call((2, 3))]) 36 | 37 | 38 | WITH_STMT_MULTI_CTX_WITH_UNPACK_SEQUENCE = """ 39 | def call(ctx1, ctx2): 40 | with ctx1() as (a, (b, c)), ctx2() as ((x, z), (s, h)): 41 | return a, b, c, x, z, s, h 42 | """ 43 | 44 | 45 | def test_with_stmt_multi_ctx_unpack_sequence(mocker): 46 | result = compile_restricted_exec(WITH_STMT_MULTI_CTX_WITH_UNPACK_SEQUENCE) 47 | assert result.errors == () 48 | 49 | @contextlib.contextmanager 50 | def ctx1(): 51 | yield (1, (2, 3)) 52 | 53 | @contextlib.contextmanager 54 | def ctx2(): 55 | yield (4, 5), (6, 7) 56 | 57 | _getiter_ = mocker.stub() 58 | _getiter_.side_effect = lambda ob: ob 59 | 60 | glb = { 61 | '_getiter_': _getiter_, 62 | '_unpack_sequence_': guarded_unpack_sequence 63 | } 64 | 65 | exec(result.code, glb) 66 | 67 | ret = glb['call'](ctx1, ctx2) 68 | 69 | assert ret == (1, 2, 3, 4, 5, 6, 7) 70 | _getiter_.assert_has_calls([ 71 | mocker.call((1, (2, 3))), 72 | mocker.call((2, 3)), 73 | mocker.call(((4, 5), (6, 7))), 74 | mocker.call((4, 5)), 75 | mocker.call((6, 7)) 76 | ]) 77 | 78 | 79 | WITH_STMT_ATTRIBUTE_ACCESS = """ 80 | def simple(ctx): 81 | with ctx as x: 82 | x.z = x.y + 1 83 | 84 | def assign_attr(ctx, x): 85 | with ctx as x.y: 86 | x.z = 1 87 | 88 | def load_attr(w): 89 | with w.ctx as x: 90 | x.z = 1 91 | 92 | """ 93 | 94 | 95 | def test_with_stmt_attribute_access(mocker): 96 | _getattr_ = mocker.stub() 97 | _getattr_.side_effect = getattr 98 | 99 | _write_ = mocker.stub() 100 | _write_.side_effect = lambda ob: ob 101 | 102 | glb = {'_getattr_': _getattr_, '_write_': _write_} 103 | restricted_exec(WITH_STMT_ATTRIBUTE_ACCESS, glb) 104 | 105 | # Test simple 106 | ctx = mocker.MagicMock(y=1) 107 | ctx.__enter__.return_value = ctx 108 | 109 | glb['simple'](ctx) 110 | 111 | assert ctx.z == 2 112 | _write_.assert_called_once_with(ctx) 113 | _getattr_.assert_called_once_with(ctx, 'y') 114 | 115 | _write_.reset_mock() 116 | _getattr_.reset_mock() 117 | 118 | # Test assign_attr 119 | x = mocker.Mock() 120 | glb['assign_attr'](ctx, x) 121 | 122 | assert x.z == 1 123 | assert x.y == ctx 124 | _write_.assert_has_calls([ 125 | mocker.call(x), 126 | mocker.call(x) 127 | ]) 128 | 129 | _write_.reset_mock() 130 | 131 | # Test load_attr 132 | ctx = mocker.MagicMock() 133 | ctx.__enter__.return_value = ctx 134 | 135 | w = mocker.Mock(ctx=ctx) 136 | 137 | glb['load_attr'](w) 138 | 139 | assert w.ctx.z == 1 140 | _getattr_.assert_called_once_with(w, 'ctx') 141 | _write_.assert_called_once_with(w.ctx) 142 | 143 | 144 | WITH_STMT_SUBSCRIPT = """ 145 | def single_key(ctx, x): 146 | with ctx as x['key']: 147 | pass 148 | 149 | 150 | def slice_key(ctx, x): 151 | with ctx as x[2:3]: 152 | pass 153 | """ 154 | 155 | 156 | def test_with_stmt_subscript(mocker): 157 | _write_ = mocker.stub() 158 | _write_.side_effect = lambda ob: ob 159 | 160 | glb = {'_write_': _write_} 161 | restricted_exec(WITH_STMT_SUBSCRIPT, glb) 162 | 163 | # Test single_key 164 | ctx = mocker.MagicMock() 165 | ctx.__enter__.return_value = ctx 166 | x = {} 167 | 168 | glb['single_key'](ctx, x) 169 | 170 | assert x['key'] == ctx 171 | _write_.assert_called_once_with(x) 172 | _write_.reset_mock() 173 | 174 | # Test slice_key 175 | ctx = mocker.MagicMock() 176 | ctx.__enter__.return_value = (1, 2) 177 | 178 | x = [0, 0, 0, 0, 0, 0] 179 | glb['slice_key'](ctx, x) 180 | 181 | assert x == [0, 0, 1, 2, 0, 0, 0] 182 | _write_.assert_called_once_with(x) 183 | -------------------------------------------------------------------------------- /tests/transformer/test_subscript.py: -------------------------------------------------------------------------------- 1 | from tests.helper import restricted_exec 2 | 3 | 4 | SIMPLE_SUBSCRIPTS = """ 5 | def simple_subscript(a): 6 | return a['b'] 7 | """ 8 | 9 | 10 | def test_read_simple_subscript(mocker): 11 | value = None 12 | _getitem_ = mocker.stub() 13 | _getitem_.side_effect = lambda ob, index: (ob, index) 14 | glb = {'_getitem_': _getitem_} 15 | restricted_exec(SIMPLE_SUBSCRIPTS, glb) 16 | 17 | assert (value, 'b') == glb['simple_subscript'](value) 18 | 19 | 20 | VAR_SUBSCRIPT = """ 21 | def simple_subscript(a, b): 22 | return a[b] 23 | """ 24 | 25 | 26 | def test_read_subscript_with_variable(mocker): 27 | value = [1] 28 | idx = 0 29 | _getitem_ = mocker.stub() 30 | _getitem_.side_effect = lambda ob, index: (ob, index) 31 | glb = {'_getitem_': _getitem_} 32 | restricted_exec(VAR_SUBSCRIPT, glb) 33 | 34 | assert (value, 0) == glb['simple_subscript'](value, idx) 35 | 36 | 37 | TUPLE_SUBSCRIPTS = """ 38 | def tuple_subscript(a): 39 | return a[1, 2] 40 | """ 41 | 42 | 43 | def test_tuple_subscript(mocker): 44 | value = None 45 | _getitem_ = mocker.stub() 46 | _getitem_.side_effect = lambda ob, index: (ob, index) 47 | glb = {'_getitem_': _getitem_} 48 | restricted_exec(TUPLE_SUBSCRIPTS, glb) 49 | 50 | assert (value, (1, 2)) == glb['tuple_subscript'](value) 51 | 52 | 53 | SLICE_SUBSCRIPT_NO_UPPER_BOUND = """ 54 | def slice_subscript_no_upper_bound(a): 55 | return a[1:] 56 | """ 57 | 58 | 59 | def test_read_slice_subscript_no_upper_bound(mocker): 60 | value = None 61 | _getitem_ = mocker.stub() 62 | _getitem_.side_effect = lambda ob, index: (ob, index) 63 | glb = {'_getitem_': _getitem_} 64 | restricted_exec(SLICE_SUBSCRIPT_NO_UPPER_BOUND, glb) 65 | 66 | assert (value, slice(1, None, None)) == glb['slice_subscript_no_upper_bound'](value) # NOQA: E501 67 | 68 | 69 | SLICE_SUBSCRIPT_NO_LOWER_BOUND = """ 70 | def slice_subscript_no_lower_bound(a): 71 | return a[:1] 72 | """ 73 | 74 | 75 | def test_read_slice_subscript_no_lower_bound(mocker): 76 | value = None 77 | _getitem_ = mocker.stub() 78 | _getitem_.side_effect = lambda ob, index: (ob, index) 79 | glb = {'_getitem_': _getitem_} 80 | restricted_exec(SLICE_SUBSCRIPT_NO_LOWER_BOUND, glb) 81 | 82 | assert (value, slice(None, 1, None)) == glb['slice_subscript_no_lower_bound'](value) # NOQA: E501 83 | 84 | 85 | SLICE_SUBSCRIPT_NO_STEP = """ 86 | def slice_subscript_no_step(a): 87 | return a[1:2] 88 | """ 89 | 90 | 91 | def test_read_slice_subscript_no_step(mocker): 92 | value = None 93 | _getitem_ = mocker.stub() 94 | _getitem_.side_effect = lambda ob, index: (ob, index) 95 | glb = {'_getitem_': _getitem_} 96 | restricted_exec(SLICE_SUBSCRIPT_NO_STEP, glb) 97 | 98 | assert (value, slice(1, 2, None)) == glb['slice_subscript_no_step'](value) 99 | 100 | 101 | SLICE_SUBSCRIPT_WITH_STEP = """ 102 | def slice_subscript_with_step(a): 103 | return a[1:2:3] 104 | """ 105 | 106 | 107 | def test_read_slice_subscript_with_step(mocker): 108 | value = None 109 | _getitem_ = mocker.stub() 110 | _getitem_.side_effect = lambda ob, index: (ob, index) 111 | glb = {'_getitem_': _getitem_} 112 | restricted_exec(SLICE_SUBSCRIPT_WITH_STEP, glb) 113 | 114 | assert (value, slice(1, 2, 3)) == glb['slice_subscript_with_step'](value) 115 | 116 | 117 | EXTENDED_SLICE_SUBSCRIPT = """ 118 | 119 | def extended_slice_subscript(a): 120 | return a[0, :1, 1:, 1:2, 1:2:3] 121 | """ 122 | 123 | 124 | def test_read_extended_slice_subscript(mocker): 125 | value = None 126 | _getitem_ = mocker.stub() 127 | _getitem_.side_effect = lambda ob, index: (ob, index) 128 | glb = {'_getitem_': _getitem_} 129 | restricted_exec(EXTENDED_SLICE_SUBSCRIPT, glb) 130 | ret = glb['extended_slice_subscript'](value) 131 | ref = ( 132 | value, 133 | ( 134 | 0, 135 | slice(None, 1, None), 136 | slice(1, None, None), 137 | slice(1, 2, None), 138 | slice(1, 2, 3) 139 | ) 140 | ) 141 | 142 | assert ref == ret 143 | 144 | 145 | WRITE_SUBSCRIPTS = """ 146 | def assign_subscript(a): 147 | a['b'] = 1 148 | """ 149 | 150 | 151 | def test_write_subscripts( 152 | mocker): 153 | value = {'b': None} 154 | _write_ = mocker.stub() 155 | _write_.side_effect = lambda ob: ob 156 | glb = {'_write_': _write_} 157 | restricted_exec(WRITE_SUBSCRIPTS, glb) 158 | 159 | glb['assign_subscript'](value) 160 | assert value['b'] == 1 161 | 162 | 163 | DEL_SUBSCRIPT = """ 164 | def del_subscript(a): 165 | del a['b'] 166 | """ 167 | 168 | 169 | def test_del_subscripts( 170 | mocker): 171 | value = {'b': None} 172 | _write_ = mocker.stub() 173 | _write_.side_effect = lambda ob: ob 174 | glb = {'_write_': _write_} 175 | restricted_exec(DEL_SUBSCRIPT, glb) 176 | glb['del_subscript'](value) 177 | 178 | assert value == {} 179 | -------------------------------------------------------------------------------- /tests/transformer/test_classdef.py: -------------------------------------------------------------------------------- 1 | from RestrictedPython import compile_restricted_exec 2 | from RestrictedPython.Guards import safe_builtins 3 | from tests.helper import restricted_exec 4 | 5 | 6 | GOOD_CLASS = ''' 7 | class Good: 8 | pass 9 | ''' 10 | 11 | 12 | def test_RestrictingNodeTransformer__visit_ClassDef__1(): 13 | """It allows to define an class.""" 14 | result = compile_restricted_exec(GOOD_CLASS) 15 | assert result.errors == () 16 | assert result.code is not None 17 | 18 | 19 | BAD_CLASS = '''\ 20 | class _bad: 21 | pass 22 | ''' 23 | 24 | 25 | def test_RestrictingNodeTransformer__visit_ClassDef__2(): 26 | """It does not allow class names which start with an underscore.""" 27 | result = compile_restricted_exec(BAD_CLASS) 28 | assert result.errors == ( 29 | 'Line 1: "_bad" is an invalid variable name ' 30 | 'because it starts with "_"',) 31 | 32 | 33 | IMPLICIT_METACLASS = ''' 34 | class Meta: 35 | pass 36 | 37 | b = Meta().foo 38 | ''' 39 | 40 | 41 | def test_RestrictingNodeTransformer__visit_ClassDef__3(): 42 | """It applies the global __metaclass__ to all generated classes if present. 43 | """ 44 | def _metaclass(name, bases, dict): 45 | ob = type(name, bases, dict) 46 | ob.foo = 2411 47 | return ob 48 | 49 | restricted_globals = dict( 50 | __metaclass__=_metaclass, 51 | __name__='implicit_metaclass', 52 | b=None, 53 | _getattr_=getattr) 54 | 55 | restricted_exec(IMPLICIT_METACLASS, restricted_globals) 56 | 57 | assert restricted_globals['b'] == 2411 58 | 59 | 60 | EXPLICIT_METACLASS = ''' 61 | class WithMeta(metaclass=MyMetaClass): 62 | pass 63 | ''' 64 | 65 | 66 | def test_RestrictingNodeTransformer__visit_ClassDef__4(): 67 | """It does not allow to pass a metaclass to class definitions.""" 68 | 69 | result = compile_restricted_exec(EXPLICIT_METACLASS) 70 | 71 | assert result.errors == ( 72 | 'Line 2: The keyword argument "metaclass" is not allowed.',) 73 | assert result.code is None 74 | 75 | 76 | DECORATED_CLASS = '''\ 77 | def wrap(cls): 78 | cls.wrap_att = 23 79 | return cls 80 | 81 | class Base: 82 | base_att = 42 83 | 84 | @wrap 85 | class Combined(Base): 86 | class_att = 2342 87 | 88 | comb = Combined() 89 | ''' 90 | 91 | 92 | def test_RestrictingNodeTransformer__visit_ClassDef__5(): 93 | """It preserves base classes and decorators for classes.""" 94 | 95 | restricted_globals = dict( 96 | comb=None, _getattr_=getattr, _write_=lambda x: x, __metaclass__=type, 97 | __name__='restricted_module', __builtins__=safe_builtins) 98 | 99 | restricted_exec(DECORATED_CLASS, restricted_globals) 100 | 101 | comb = restricted_globals['comb'] 102 | assert comb.class_att == 2342 103 | assert comb.base_att == 42 104 | assert comb.wrap_att == 23 105 | 106 | 107 | CONSTRUCTOR_TEST = """\ 108 | class Test: 109 | def __init__(self, input): 110 | self.input = input 111 | 112 | t = Test(42) 113 | """ 114 | 115 | 116 | def test_RestrictingNodeTransformer__visit_ClassDef__6(): 117 | """It allows to define an ``__init__`` method.""" 118 | restricted_globals = dict( 119 | t=None, 120 | _write_=lambda x: x, 121 | __metaclass__=type, 122 | __name__='constructor_test', 123 | ) 124 | 125 | restricted_exec(CONSTRUCTOR_TEST, restricted_globals) 126 | t = restricted_globals['t'] 127 | assert t.input == 42 128 | 129 | 130 | COMPARE_TEST = """\ 131 | class Test: 132 | 133 | def __init__(self, value): 134 | self.value = value 135 | 136 | def __eq__(self, other): 137 | return self.value == other.value 138 | 139 | a = Test(42) 140 | b = Test(42) 141 | c = Test(43) 142 | 143 | result1 = (a == b) 144 | result2 = (b == c) 145 | """ 146 | 147 | 148 | def test_RestrictingNodeTransformer__visit_ClassDef__7(): 149 | """It allows to define an ``__eq__`` method.""" 150 | restricted_globals = dict( 151 | result1=None, 152 | result2=None, 153 | _getattr_=getattr, 154 | _write_=lambda x: x, 155 | __metaclass__=type, 156 | __name__='compare_test', 157 | ) 158 | 159 | restricted_exec(COMPARE_TEST, restricted_globals) 160 | assert restricted_globals['result1'] is True 161 | assert restricted_globals['result2'] is False 162 | 163 | 164 | CONTAINER_TEST = """\ 165 | class Test(object): 166 | 167 | def __init__(self, values): 168 | self.values = values 169 | 170 | def __contains__(self, value): 171 | return value in self.values 172 | 173 | a = Test([1, 2, 3]) 174 | 175 | result1 = (1 in a) 176 | result2 = (4 not in a) 177 | """ 178 | 179 | 180 | def test_RestrictingNodeTransformer__visit_ClassDef__8(): 181 | """It allows to define a ``__contains__`` method.""" 182 | restricted_globals = dict( 183 | result1=None, 184 | result2=None, 185 | _getattr_=getattr, 186 | _write_=lambda x: x, 187 | __metaclass__=type, 188 | __name__='container_test', 189 | object=object, 190 | ) 191 | 192 | restricted_exec(CONTAINER_TEST, restricted_globals) 193 | assert restricted_globals['result1'] is True 194 | assert restricted_globals['result2'] is True 195 | -------------------------------------------------------------------------------- /docs/usage/basic_usage.rst: -------------------------------------------------------------------------------- 1 | Basic usage 2 | ----------- 3 | 4 | The general workflow to execute Python code that is loaded within a Python program is: 5 | 6 | .. testcode:: 7 | 8 | source_code = """ 9 | def do_something(): 10 | pass 11 | """ 12 | 13 | byte_code = compile(source_code, filename='', mode='exec') 14 | exec(byte_code) 15 | do_something() 16 | 17 | With RestrictedPython that workflow should be as straight forward as possible: 18 | 19 | .. testcode:: 20 | 21 | from RestrictedPython import compile_restricted 22 | 23 | source_code = """ 24 | def do_something(): 25 | pass 26 | """ 27 | 28 | byte_code = compile_restricted( 29 | source_code, 30 | filename='', 31 | mode='exec' 32 | ) 33 | exec(byte_code) 34 | do_something() 35 | 36 | You might also use the replacement import: 37 | 38 | .. testcode:: 39 | 40 | from RestrictedPython import compile_restricted as compile 41 | 42 | ``compile_restricted`` uses a predefined policy that checks and modify the source code and checks against a restricted subset of the Python language. 43 | The compiled source code is still executed against the full available set of library modules and methods. 44 | 45 | The Python :py:func:`exec` takes three parameters: 46 | 47 | * ``code`` which is the compiled byte code 48 | * ``globals`` which is global dictionary 49 | * ``locals`` which is the local dictionary 50 | 51 | By limiting the entries in the ``globals`` and ``locals`` dictionaries you 52 | restrict the access to the available library modules and methods. 53 | 54 | Providing defined dictionaries for ``exec()`` should be used in context of RestrictedPython. 55 | 56 | .. code-block:: python 57 | 58 | byte_code = 59 | exec(byte_code, { ... }, { ... }) 60 | 61 | Typically there is a defined set of allowed modules, methods and constants used in that context. 62 | RestrictedPython provides three predefined built-ins for that (see :ref:`predefined_builtins` for details): 63 | 64 | * ``safe_builtins`` 65 | * ``limited_builtins`` 66 | * ``utility_builtins`` 67 | 68 | So you normally end up using: 69 | 70 | .. testcode:: 71 | 72 | from RestrictedPython import compile_restricted 73 | 74 | from RestrictedPython import safe_builtins 75 | from RestrictedPython import limited_builtins 76 | from RestrictedPython import utility_builtins 77 | 78 | source_code = """ 79 | def do_something(): 80 | pass 81 | """ 82 | 83 | try: 84 | byte_code = compile_restricted( 85 | source_code, 86 | filename='', 87 | mode='exec' 88 | ) 89 | exec(byte_code, {'__builtins__': safe_builtins}, None) 90 | except SyntaxError as e: 91 | pass 92 | 93 | One common advanced usage would be to define an own restricted builtin dictionary. 94 | 95 | There is a shortcut for ``{'__builtins__': safe_builtins}`` named ``safe_globals`` which can be imported from ``RestrictedPython``. 96 | 97 | Other Usages 98 | ------------ 99 | 100 | RestrictedPython has similar to normal Python multiple modes: 101 | 102 | * exec 103 | * eval 104 | * single 105 | * function 106 | 107 | you can use it by: 108 | 109 | .. testcode:: 110 | 111 | from RestrictedPython import compile_restricted 112 | 113 | source_code = """ 114 | def do_something(): 115 | pass 116 | """ 117 | 118 | byte_code = compile_restricted( 119 | source_code, 120 | filename='', 121 | mode='exec' 122 | ) 123 | exec(byte_code) 124 | do_something() 125 | 126 | .. testcode:: 127 | 128 | from RestrictedPython import compile_restricted 129 | 130 | byte_code = compile_restricted( 131 | "2 + 2", 132 | filename='', 133 | mode='eval' 134 | ) 135 | eval(byte_code) 136 | 137 | 138 | .. testcode:: single 139 | 140 | from RestrictedPython import compile_restricted 141 | 142 | byte_code = compile_restricted( 143 | "2 + 2", 144 | filename='', 145 | mode='single' 146 | ) 147 | exec(byte_code) 148 | 149 | .. testoutput:: single 150 | 151 | 4 152 | 153 | Necessary setup 154 | --------------- 155 | 156 | `RestrictedPython` requires some predefined names in globals in order to work 157 | properly. 158 | 159 | To use classes in Python 3 160 | * ``__metaclass__`` must be set. Set it to ``type`` to use no custom metaclass. 161 | * ``__name__`` must be set. As classes need a namespace to be defined in. 162 | It is the name of the module the class is defined in. You might set it to 163 | an arbitrary string. 164 | 165 | To use ``for`` statements and comprehensions: 166 | * ``_getiter_`` must point to an ``iter`` implementation. As an unguarded variant you might use 167 | :func:`RestrictedPython.Eval.default_guarded_getiter`. 168 | 169 | * ``_iter_unpack_sequence_`` must point to :func:`RestrictedPython.Guards.guarded_iter_unpack_sequence`. 170 | 171 | To use ``getattr`` 172 | you have to provide an implementation for it. 173 | :func:`RestrictedPython.Guards.safer_getattr` can be a starting point. 174 | 175 | The usage of `RestrictedPython` in :mod:`AccessControl.ZopeGuards` can serve as example. 176 | -------------------------------------------------------------------------------- /tests/builtins/test_utilities.py: -------------------------------------------------------------------------------- 1 | import math 2 | import random 3 | import string 4 | 5 | 6 | def test_string_in_utility_builtins(): 7 | from RestrictedPython.Utilities import utility_builtins 8 | 9 | # we no longer provide access to ``string`` itself, only to 10 | # a restricted view of it (``rstring``) 11 | rstring = utility_builtins['string'] 12 | assert rstring.__name__ == string.__name__ 13 | 14 | # ensure it does not provide access to ``string`` via 15 | # ``AttributeError.obj`` 16 | try: 17 | rstring.unexisting_attribute 18 | except AttributeError as e: 19 | assert e.obj is rstring 20 | 21 | 22 | def test_math_in_utility_builtins(): 23 | from RestrictedPython.Utilities import utility_builtins 24 | assert utility_builtins['math'] is math 25 | 26 | 27 | def test_whrandom_in_utility_builtins(): 28 | from RestrictedPython.Utilities import utility_builtins 29 | assert utility_builtins['whrandom'] is random 30 | 31 | 32 | def test_random_in_utility_builtins(): 33 | from RestrictedPython.Utilities import utility_builtins 34 | assert utility_builtins['random'] is random 35 | 36 | 37 | def test_set_in_utility_builtins(): 38 | from RestrictedPython.Utilities import utility_builtins 39 | assert utility_builtins['set'] is set 40 | 41 | 42 | def test_frozenset_in_utility_builtins(): 43 | from RestrictedPython.Utilities import utility_builtins 44 | assert utility_builtins['frozenset'] is frozenset 45 | 46 | 47 | def test_DateTime_in_utility_builtins_if_importable(): 48 | try: 49 | import DateTime 50 | except ImportError: 51 | pass 52 | else: 53 | from RestrictedPython.Utilities import utility_builtins 54 | assert DateTime.__name__ in utility_builtins 55 | 56 | 57 | def test_same_type_in_utility_builtins(): 58 | from RestrictedPython.Utilities import same_type 59 | from RestrictedPython.Utilities import utility_builtins 60 | assert utility_builtins['same_type'] is same_type 61 | 62 | 63 | def test_test_in_utility_builtins(): 64 | from RestrictedPython.Utilities import test 65 | from RestrictedPython.Utilities import utility_builtins 66 | assert utility_builtins['test'] is test 67 | 68 | 69 | def test_reorder_in_utility_builtins(): 70 | from RestrictedPython.Utilities import reorder 71 | from RestrictedPython.Utilities import utility_builtins 72 | assert utility_builtins['reorder'] is reorder 73 | 74 | 75 | def test_sametype_only_one_arg(): 76 | from RestrictedPython.Utilities import same_type 77 | assert same_type(object()) 78 | 79 | 80 | def test_sametype_only_two_args_same(): 81 | from RestrictedPython.Utilities import same_type 82 | assert same_type(object(), object()) 83 | 84 | 85 | def test_sametype_only_two_args_different(): 86 | from RestrictedPython.Utilities import same_type 87 | 88 | class Foo: 89 | pass 90 | assert same_type(object(), Foo()) is False 91 | 92 | 93 | def test_sametype_only_multiple_args_same(): 94 | from RestrictedPython.Utilities import same_type 95 | assert same_type(object(), object(), object(), object()) 96 | 97 | 98 | def test_sametype_only_multipe_args_one_different(): 99 | from RestrictedPython.Utilities import same_type 100 | 101 | class Foo: 102 | pass 103 | assert same_type(object(), object(), Foo()) is False 104 | 105 | 106 | def test_test_single_value_true(): 107 | from RestrictedPython.Utilities import test 108 | assert test(True) is True 109 | 110 | 111 | def test_test_single_value_False(): 112 | from RestrictedPython.Utilities import test 113 | assert test(False) is False 114 | 115 | 116 | def test_test_even_values_first_true(): 117 | from RestrictedPython.Utilities import test 118 | assert test(True, 'first', True, 'second') == 'first' 119 | 120 | 121 | def test_test_even_values_not_first_true(): 122 | from RestrictedPython.Utilities import test 123 | assert test(False, 'first', True, 'second') == 'second' 124 | 125 | 126 | def test_test_odd_values_first_true(): 127 | from RestrictedPython.Utilities import test 128 | assert test(True, 'first', True, 'second', False) == 'first' 129 | 130 | 131 | def test_test_odd_values_not_first_true(): 132 | from RestrictedPython.Utilities import test 133 | assert test(False, 'first', True, 'second', False) == 'second' 134 | 135 | 136 | def test_test_odd_values_last_true(): 137 | from RestrictedPython.Utilities import test 138 | assert test(False, 'first', False, 'second', 'third') == 'third' 139 | 140 | 141 | def test_test_odd_values_last_false(): 142 | from RestrictedPython.Utilities import test 143 | assert test(False, 'first', False, 'second', False) is False 144 | 145 | 146 | def test_reorder_with__None(): 147 | from RestrictedPython.Utilities import reorder 148 | before = ['a', 'b', 'c', 'd', 'e'] 149 | without = ['a', 'c', 'e'] 150 | after = reorder(before, without=without) 151 | assert after == [('b', 'b'), ('d', 'd')] 152 | 153 | 154 | def test_reorder_with__not_None(): 155 | from RestrictedPython.Utilities import reorder 156 | before = ['a', 'b', 'c', 'd', 'e'] 157 | with_ = ['a', 'd'] 158 | without = ['a', 'c', 'e'] 159 | after = reorder(before, with_=with_, without=without) 160 | assert after == [('d', 'd')] 161 | -------------------------------------------------------------------------------- /tests/transformer/test_name.py: -------------------------------------------------------------------------------- 1 | from RestrictedPython import compile_restricted_exec 2 | from tests.helper import restricted_exec 3 | 4 | 5 | BAD_NAME_STARTING_WITH_UNDERSCORE = """\ 6 | def bad_name(): 7 | __ = 12 8 | """ 9 | 10 | 11 | def test_RestrictingNodeTransformer__visit_Name__1(): 12 | """It denies a variable name starting in `__`.""" 13 | result = compile_restricted_exec(BAD_NAME_STARTING_WITH_UNDERSCORE) 14 | assert result.errors == ( 15 | 'Line 2: "__" is an invalid variable name because it starts with "_"',) 16 | 17 | 18 | BAD_NAME_OVERRIDE_GUARD_WITH_NAME = """\ 19 | def overrideGuardWithName(): 20 | _getattr = None 21 | """ 22 | 23 | 24 | def test_RestrictingNodeTransformer__visit_Name__2(): 25 | """It denies a variable name starting in `_`.""" 26 | result = compile_restricted_exec(BAD_NAME_OVERRIDE_GUARD_WITH_NAME) 27 | assert result.errors == ( 28 | 'Line 2: "_getattr" is an invalid variable name because ' 29 | 'it starts with "_"',) 30 | 31 | 32 | def test_RestrictingNodeTransformer__visit_Name__2_5(): 33 | """It allows `_` as variable name.""" 34 | glb = restricted_exec('_ = 2411') 35 | assert glb['_'] == 2411 36 | 37 | 38 | BAD_NAME_OVERRIDE_OVERRIDE_GUARD_WITH_FUNCTION = """\ 39 | def overrideGuardWithFunction(): 40 | def _getattr(o): 41 | return o 42 | """ 43 | 44 | 45 | def test_RestrictingNodeTransformer__visit_Name__3(): 46 | """It denies a function name starting in `_`.""" 47 | result = compile_restricted_exec( 48 | BAD_NAME_OVERRIDE_OVERRIDE_GUARD_WITH_FUNCTION) 49 | assert result.errors == ( 50 | 'Line 2: "_getattr" is an invalid variable name because it ' 51 | 'starts with "_"',) 52 | 53 | 54 | BAD_NAME_OVERRIDE_GUARD_WITH_CLASS = """\ 55 | def overrideGuardWithClass(): 56 | class _getattr: 57 | pass 58 | """ 59 | 60 | 61 | def test_RestrictingNodeTransformer__visit_Name__4(): 62 | """It denies a class name starting in `_`.""" 63 | result = compile_restricted_exec(BAD_NAME_OVERRIDE_GUARD_WITH_CLASS) 64 | assert result.errors == ( 65 | 'Line 2: "_getattr" is an invalid variable name because it ' 66 | 'starts with "_"',) 67 | 68 | 69 | BAD_NAME_IN_WITH = """\ 70 | def with_as_bad_name(): 71 | with x as _leading_underscore: 72 | pass 73 | """ 74 | 75 | 76 | def test_RestrictingNodeTransformer__visit_Name__4_4(): 77 | """It denies a variable name in with starting in `_`.""" 78 | result = compile_restricted_exec(BAD_NAME_IN_WITH) 79 | assert result.errors == ( 80 | 'Line 2: "_leading_underscore" is an invalid variable name because ' 81 | 'it starts with "_"',) 82 | 83 | 84 | BAD_NAME_IN_COMPOUND_WITH = """\ 85 | def compound_with_bad_name(): 86 | with a as b, c as _restricted_name: 87 | pass 88 | """ 89 | 90 | 91 | def test_RestrictingNodeTransformer__visit_Name__4_5(): 92 | """It denies a variable name in with starting in `_`.""" 93 | result = compile_restricted_exec(BAD_NAME_IN_COMPOUND_WITH) 94 | assert result.errors == ( 95 | 'Line 2: "_restricted_name" is an invalid variable name because ' 96 | 'it starts with "_"',) 97 | 98 | 99 | BAD_NAME_DICT_COMP = """\ 100 | def dict_comp_bad_name(): 101 | {y: y for _restricted_name in x} 102 | """ 103 | 104 | 105 | def test_RestrictingNodeTransformer__visit_Name__4_6(): 106 | """It denies a variable name starting in `_` in a dict comprehension.""" 107 | result = compile_restricted_exec(BAD_NAME_DICT_COMP) 108 | assert result.errors == ( 109 | 'Line 2: "_restricted_name" is an invalid variable name because ' 110 | 'it starts with "_"',) 111 | 112 | 113 | BAD_NAME_SET_COMP = """\ 114 | def set_comp_bad_name(): 115 | {y for _restricted_name in x} 116 | """ 117 | 118 | 119 | def test_RestrictingNodeTransformer__visit_Name__4_7(): 120 | """It denies a variable name starting in `_` in a dict comprehension.""" 121 | result = compile_restricted_exec(BAD_NAME_SET_COMP) 122 | assert result.errors == ( 123 | 'Line 2: "_restricted_name" is an invalid variable name because ' 124 | 'it starts with "_"',) 125 | 126 | 127 | BAD_NAME_ENDING_WITH___ROLES__ = """\ 128 | def bad_name(): 129 | myvar__roles__ = 12 130 | """ 131 | 132 | 133 | def test_RestrictingNodeTransformer__visit_Name__5(): 134 | """It denies a variable name ending in `__roles__`.""" 135 | result = compile_restricted_exec(BAD_NAME_ENDING_WITH___ROLES__) 136 | assert result.errors == ( 137 | 'Line 2: "myvar__roles__" is an invalid variable name because it ' 138 | 'ends with "__roles__".',) 139 | 140 | 141 | BAD_NAME_PRINTED = """\ 142 | def bad_name(): 143 | printed = 12 144 | """ 145 | 146 | 147 | def test_RestrictingNodeTransformer__visit_Name__6(): 148 | """It denies a variable named `printed`.""" 149 | result = compile_restricted_exec(BAD_NAME_PRINTED) 150 | assert result.errors == ('Line 2: "printed" is a reserved name.',) 151 | 152 | 153 | BAD_NAME_PRINT = """\ 154 | def bad_name(): 155 | def print(): 156 | pass 157 | """ 158 | 159 | 160 | def test_RestrictingNodeTransformer__visit_Name__7(): 161 | """It denies a variable named `print`.""" 162 | result = compile_restricted_exec(BAD_NAME_PRINT) 163 | assert result.errors == ('Line 2: "print" is a reserved name.',) 164 | -------------------------------------------------------------------------------- /docs/usage/api.rst: -------------------------------------------------------------------------------- 1 | API overview 2 | ------------ 3 | 4 | ``compile_restricted`` methods 5 | ++++++++++++++++++++++++++++++ 6 | 7 | .. py:method:: compile_restricted(source, filename, mode, flags, dont_inherit, policy) 8 | :module: RestrictedPython 9 | 10 | Compiles source code into interpretable byte code. 11 | 12 | :param source: (required). the source code that should be compiled 13 | :param filename: (optional). defaults to ``''`` 14 | :param mode: (optional). Use ``'exec'``, ``'eval'``, ``'single'`` or ``'function'``. defaults to ``'exec'`` 15 | :param flags: (optional). defaults to ``0`` 16 | :param dont_inherit: (optional). defaults to ``False`` 17 | :param policy: (optional). defaults to ``RestrictingNodeTransformer`` 18 | :type source: str or unicode text or ``ast.AST`` 19 | :type filename: str or unicode text 20 | :type mode: str or unicode text 21 | :type flags: int 22 | :type dont_inherit: int 23 | :type policy: RestrictingNodeTransformer class 24 | :return: Python ``code`` object 25 | 26 | .. py:method:: compile_restricted_exec(source, filename, flags, dont_inherit, policy) 27 | :module: RestrictedPython 28 | 29 | Compiles source code into interpretable byte code with ``mode='exec'``. 30 | Use mode ``'exec'`` if the source contains a sequence of statements. 31 | The meaning and defaults of the parameters are the same as in 32 | ``compile_restricted``. 33 | 34 | :return: CompileResult (a namedtuple with code, errors, warnings, used_names) 35 | 36 | .. py:method:: compile_restricted_eval(source, filename, flags, dont_inherit, policy) 37 | :module: RestrictedPython 38 | 39 | Compiles source code into interpretable byte code with ``mode='eval'``. 40 | Use mode ``'eval'`` if the source contains a single expression. 41 | The meaning and defaults of the parameters are the same as in 42 | ``compile_restricted``. 43 | 44 | :return: CompileResult (a namedtuple with code, errors, warnings, used_names) 45 | 46 | .. py:method:: compile_restricted_single(source, filename, flags, dont_inherit, policy) 47 | :module: RestrictedPython 48 | 49 | Compiles source code into interpretable byte code with ``mode='eval'``. 50 | Use mode ``'single'`` if the source contains a single interactive statement. 51 | The meaning and defaults of the parameters are the same as in 52 | ``compile_restricted``. 53 | 54 | :return: CompileResult (a namedtuple with code, errors, warnings, used_names) 55 | 56 | .. py:method:: compile_restricted_function(p, body, name, filename, globalize=None) 57 | :module: RestrictedPython 58 | 59 | Compiles source code into interpretable byte code with ``mode='function'``. 60 | Use mode ``'function'`` for full functions. 61 | 62 | :param p: (required). a string representing the function parameters 63 | :param body: (required). the function body 64 | :param name: (required). the function name 65 | :param filename: (optional). defaults to ``''`` 66 | :param globalize: (optional). list of globals. defaults to ``None`` 67 | :param flags: (optional). defaults to ``0`` 68 | :param dont_inherit: (optional). defaults to ``False`` 69 | :param policy: (optional). defaults to ``RestrictingNodeTransformer`` 70 | :type p: str or unicode text 71 | :type body: str or unicode text 72 | :type name: str or unicode text 73 | :type filename: str or unicode text 74 | :type globalize: None or list 75 | :type flags: int 76 | :type dont_inherit: int 77 | :type policy: RestrictingNodeTransformer class 78 | :return: byte code 79 | 80 | The globalize argument, if specified, is a list of variable names to be 81 | treated as globals (code is generated as if each name in the list 82 | appeared in a global statement at the top of the function). 83 | This allows to inject global variables into the generated function that 84 | feel like they are local variables, so the programmer who uses this doesn't 85 | have to understand that his code is executed inside a function scope 86 | instead of the global scope of a module. 87 | 88 | To actually get an executable function, you need to execute this code and 89 | pull out the defined function out of the locals like this: 90 | 91 | >>> from RestrictedPython import compile_restricted_function 92 | >>> compiled = compile_restricted_function('', 'pass', 'function_name') 93 | >>> safe_locals = {} 94 | >>> safe_globals = {} 95 | >>> exec(compiled.code, safe_globals, safe_locals) 96 | >>> compiled_function = safe_locals['function_name'] 97 | >>> result = compiled_function(*[], **{}) 98 | 99 | Then if you want to control the globals for a specific call to this 100 | function, you can regenerate the function like this: 101 | 102 | >>> my_call_specific_global_bindings = dict(foo='bar') 103 | >>> safe_globals = safe_globals.copy() 104 | >>> safe_globals.update(my_call_specific_global_bindings) 105 | >>> import types 106 | >>> new_function = types.FunctionType( 107 | ... compiled_function.__code__, 108 | ... safe_globals, 109 | ... '', 110 | ... compiled_function.__defaults__ or ()) 111 | >>> result = new_function(*[], **{}) 112 | 113 | restricted builtins 114 | +++++++++++++++++++ 115 | 116 | * ``safe_globals`` 117 | * ``safe_builtins`` 118 | * ``limited_builtins`` 119 | * ``utility_builtins`` 120 | 121 | helper modules 122 | ++++++++++++++ 123 | 124 | * ``PrintCollector`` 125 | 126 | 127 | RestrictingNodeTransformer 128 | ++++++++++++++++++++++++++ 129 | 130 | ``RestrictingNodeTransformer`` provides the base policy used by RestrictedPython itself. 131 | 132 | It is a subclass of a ``NodeTransformer`` which has a set of ``visit_`` methods and a ``generic_visit`` method. 133 | 134 | ``generic_visit`` is a predefined method of any ``NodeVisitor`` which sequentially visits all sub nodes. In RestrictedPython this behaviour is overwritten to always call a new internal method ``not_allowed(node)``. 135 | This results in an implicit blacklisting of all not allowed AST elements. 136 | 137 | Any possibly new introduced AST element in Python (new language element) will implicitly be blocked and not allowed in RestrictedPython. 138 | 139 | So, if new elements should be introduced, an explicit ``visit_`` is necessary. 140 | -------------------------------------------------------------------------------- /docs/contributing/ast/python3_9.ast: -------------------------------------------------------------------------------- 1 | -- Python 3.9 AST 2 | -- ASDL's 4 builtin types are: 3 | -- identifier, int, string, constant 4 | 5 | module Python version "3.9" 6 | { 7 | mod = Module(stmt* body, type_ignore* type_ignores) 8 | | Interactive(stmt* body) 9 | | Expression(expr body) 10 | | FunctionType(expr* argtypes, expr returns) 11 | 12 | stmt = FunctionDef(identifier name, 13 | arguments args, 14 | stmt* body, 15 | expr* decorator_list, 16 | expr? returns, 17 | string? type_comment) 18 | | AsyncFunctionDef(identifier name, 19 | arguments args, 20 | stmt* body, 21 | expr* decorator_list, 22 | expr? returns, 23 | string? type_comment) 24 | 25 | | ClassDef(identifier name, 26 | expr* bases, 27 | keyword* keywords, 28 | stmt* body, 29 | expr* decorator_list) 30 | | Return(expr? value) 31 | 32 | | Delete(expr* targets) 33 | | Assign(expr* targets, expr value, string? type_comment) 34 | | AugAssign(expr target, operator op, expr value) 35 | -- 'simple' indicates that we annotate simple name without parens 36 | | AnnAssign(expr target, expr annotation, expr? value, int simple) 37 | 38 | -- use 'orelse' because else is a keyword in target languages 39 | | For(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) 40 | | AsyncFor(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) 41 | | While(expr test, stmt* body, stmt* orelse) 42 | | If(expr test, stmt* body, stmt* orelse) 43 | | With(withitem* items, stmt* body, string? type_comment) 44 | | AsyncWith(withitem* items, stmt* body, string? type_comment) 45 | 46 | | Raise(expr? exc, expr? cause) 47 | | Try(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody) 48 | | Assert(expr test, expr? msg) 49 | 50 | | Import(alias* names) 51 | | ImportFrom(identifier? module, alias* names, int? level) 52 | 53 | | Global(identifier* names) 54 | | Nonlocal(identifier* names) 55 | | Expr(expr value) 56 | | Pass 57 | | Break 58 | | Continue 59 | 60 | -- col_offset is the byte offset in the utf8 string the parser uses 61 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 62 | 63 | -- BoolOp() can use left & right? 64 | expr = BoolOp(boolop op, expr* values) 65 | | NamedExpr(expr target, expr value) 66 | | BinOp(expr left, operator op, expr right) 67 | | UnaryOp(unaryop op, expr operand) 68 | | Lambda(arguments args, expr body) 69 | | IfExp(expr test, expr body, expr orelse) 70 | | Dict(expr* keys, expr* values) 71 | | Set(expr* elts) 72 | | ListComp(expr elt, comprehension* generators) 73 | | SetComp(expr elt, comprehension* generators) 74 | | DictComp(expr key, expr value, comprehension* generators) 75 | | GeneratorExp(expr elt, comprehension* generators) 76 | -- the grammar constrains where yield expressions can occur 77 | | Await(expr value) 78 | | Yield(expr? value) 79 | | YieldFrom(expr value) 80 | -- need sequences for compare to distinguish between 81 | -- x < 4 < 3 and (x < 4) < 3 82 | | Compare(expr left, cmpop* ops, expr* comparators) 83 | | Call(expr func, expr* args, keyword* keywords) 84 | | FormattedValue(expr value, int? conversion, expr? format_spec) 85 | | JoinedStr(expr* values) 86 | | Constant(constant value, string? kind) 87 | 88 | -- the following expression can appear in assignment context 89 | | Attribute(expr value, identifier attr, expr_context ctx) 90 | | Subscript(expr value, expr slice, expr_context ctx) 91 | | Starred(expr value, expr_context ctx) 92 | | Name(identifier id, expr_context ctx) 93 | | List(expr* elts, expr_context ctx) 94 | | Tuple(expr* elts, expr_context ctx) 95 | 96 | -- can appear only in Subscript 97 | | Slice(expr? lower, expr? upper, expr? step) 98 | 99 | -- col_offset is the byte offset in the utf8 string the parser uses 100 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 101 | 102 | expr_context = Load 103 | | Store 104 | | Del 105 | 106 | boolop = And 107 | | Or 108 | 109 | operator = Add 110 | | Sub 111 | | Mult 112 | | MatMult 113 | | Div 114 | | Mod 115 | | Pow 116 | | LShift 117 | | RShift 118 | | BitOr 119 | | BitXor 120 | | BitAnd 121 | | FloorDiv 122 | 123 | unaryop = Invert 124 | | Not 125 | | UAdd 126 | | USub 127 | 128 | cmpop = Eq 129 | | NotEq 130 | | Lt 131 | | LtE 132 | | Gt 133 | | GtE 134 | | Is 135 | | IsNot 136 | | In 137 | | NotIn 138 | 139 | comprehension = (expr target, expr iter, expr* ifs, int is_async) 140 | 141 | excepthandler = ExceptHandler(expr? type, identifier? name, stmt* body) 142 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 143 | 144 | arguments = (arg* posonlyargs, 145 | arg* args, 146 | arg? vararg, 147 | arg* kwonlyargs, 148 | expr* kw_defaults, 149 | arg? kwarg, 150 | expr* defaults) 151 | 152 | arg = (identifier arg, expr? annotation, string? type_comment) 153 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 154 | 155 | -- keyword arguments supplied to call (NULL identifier for **kwargs) 156 | keyword = (identifier? arg, expr value) 157 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 158 | 159 | -- import name with optional 'as' alias. 160 | alias = (identifier name, identifier? asname) 161 | 162 | withitem = (expr context_expr, expr? optional_vars) 163 | 164 | type_ignore = TypeIgnore(int lineno, string tag) 165 | } 166 | -------------------------------------------------------------------------------- /docs/contributing/ast/python3_8.ast: -------------------------------------------------------------------------------- 1 | -- Python 3.8 AST 2 | -- ASDL's 5 builtin types are: 3 | -- identifier, int, string, object, constant 4 | 5 | module Python version "3.8" 6 | { 7 | mod = Module(stmt* body, type_ignore *type_ignores) 8 | | Interactive(stmt* body) 9 | | Expression(expr body) 10 | | FunctionType(expr* argtypes, expr returns) 11 | 12 | -- not really an actual node but useful in Jython's typesystem. 13 | | Suite(stmt* body) 14 | 15 | stmt = FunctionDef(identifier name, 16 | arguments args, 17 | stmt* body, 18 | expr* decorator_list, 19 | expr? returns, 20 | string? type_comment) 21 | | AsyncFunctionDef(identifier name, 22 | arguments args, 23 | stmt* body, 24 | expr* decorator_list, 25 | expr? returns, 26 | string? type_comment) 27 | 28 | | ClassDef(identifier name, 29 | expr* bases, 30 | keyword* keywords, 31 | stmt* body, 32 | expr* decorator_list) 33 | | Return(expr? value) 34 | 35 | | Delete(expr* targets) 36 | | Assign(expr* targets, expr value, string? type_comment) 37 | | AugAssign(expr target, operator op, expr value) 38 | -- 'simple' indicates that we annotate simple name without parens 39 | | AnnAssign(expr target, expr annotation, expr? value, int simple) 40 | 41 | -- use 'orelse' because else is a keyword in target languages 42 | | For(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) 43 | | AsyncFor(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) 44 | | While(expr test, stmt* body, stmt* orelse) 45 | | If(expr test, stmt* body, stmt* orelse) 46 | | With(withitem* items, stmt* body, string? type_comment) 47 | | AsyncWith(withitem* items, stmt* body, string? type_comment) 48 | 49 | | Raise(expr? exc, expr? cause) 50 | | Try(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody) 51 | | Assert(expr test, expr? msg) 52 | 53 | | Import(alias* names) 54 | | ImportFrom(identifier? module, alias* names, int? level) 55 | 56 | | Global(identifier* names) 57 | | Nonlocal(identifier* names) 58 | | Expr(expr value) 59 | | Pass 60 | | Break 61 | | Continue 62 | 63 | -- XXX Jython will be different 64 | -- col_offset is the byte offset in the utf8 string the parser uses 65 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 66 | 67 | -- BoolOp() can use left & right? 68 | expr = BoolOp(boolop op, expr* values) 69 | | NamedExpr(expr target, expr value) 70 | | BinOp(expr left, operator op, expr right) 71 | | UnaryOp(unaryop op, expr operand) 72 | | Lambda(arguments args, expr body) 73 | | IfExp(expr test, expr body, expr orelse) 74 | | Dict(expr* keys, expr* values) 75 | | Set(expr* elts) 76 | | ListComp(expr elt, comprehension* generators) 77 | | SetComp(expr elt, comprehension* generators) 78 | | DictComp(expr key, expr value, comprehension* generators) 79 | | GeneratorExp(expr elt, comprehension* generators) 80 | -- the grammar constrains where yield expressions can occur 81 | | Await(expr value) 82 | | Yield(expr? value) 83 | | YieldFrom(expr value) 84 | -- need sequences for compare to distinguish between 85 | -- x < 4 < 3 and (x < 4) < 3 86 | | Compare(expr left, cmpop* ops, expr* comparators) 87 | | Call(expr func, 88 | expr* args, 89 | keyword* keywords) 90 | | FormattedValue(expr value, int? conversion, expr? format_spec) 91 | | JoinedStr(expr* values) 92 | | Constant(constant value, string? kind) 93 | 94 | -- the following expression can appear in assignment context 95 | | Attribute(expr value, identifier attr, expr_context ctx) 96 | | Subscript(expr value, slice slice, expr_context ctx) 97 | | Starred(expr value, expr_context ctx) 98 | | Name(identifier id, expr_context ctx) 99 | | List(expr* elts, expr_context ctx) 100 | | Tuple(expr* elts, expr_context ctx) 101 | 102 | -- col_offset is the byte offset in the utf8 string the parser uses 103 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 104 | 105 | expr_context = Load 106 | | Store 107 | | Del 108 | | AugLoad 109 | | AugStore 110 | | Param 111 | 112 | slice = Slice(expr? lower, expr? upper, expr? step) 113 | | ExtSlice(slice* dims) 114 | | Index(expr value) 115 | 116 | boolop = And 117 | | Or 118 | 119 | operator = Add 120 | | Sub 121 | | Mult 122 | | MatMult 123 | | Div 124 | | Mod 125 | | Pow 126 | | LShift 127 | | RShift 128 | | BitOr 129 | | BitXor 130 | | BitAnd 131 | | FloorDiv 132 | 133 | unaryop = Invert 134 | | Not 135 | | UAdd 136 | | USub 137 | 138 | cmpop = Eq 139 | | NotEq 140 | | Lt 141 | | LtE 142 | | Gt 143 | | GtE 144 | | Is 145 | | IsNot 146 | | In 147 | | NotIn 148 | 149 | comprehension = (expr target, expr iter, expr* ifs, int is_async) 150 | 151 | excepthandler = ExceptHandler(expr? type, identifier? name, stmt* body) 152 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 153 | 154 | arguments = (arg* posonlyargs, 155 | arg* args, 156 | arg? vararg, 157 | arg* kwonlyargs, 158 | expr* kw_defaults, 159 | arg? kwarg, 160 | expr* defaults) 161 | 162 | arg = (identifier arg, expr? annotation, string? type_comment) 163 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 164 | 165 | -- keyword arguments supplied to call (NULL identifier for **kwargs) 166 | keyword = (identifier? arg, expr value) 167 | 168 | -- import name with optional 'as' alias. 169 | alias = (identifier name, identifier? asname) 170 | 171 | withitem = (expr context_expr, expr? optional_vars) 172 | 173 | type_ignore = TypeIgnore(int lineno, string tag) 174 | } 175 | -------------------------------------------------------------------------------- /tests/test_compile_restricted_function.py: -------------------------------------------------------------------------------- 1 | from types import FunctionType 2 | 3 | from RestrictedPython import PrintCollector 4 | from RestrictedPython import compile_restricted_function 5 | from RestrictedPython import safe_builtins 6 | from RestrictedPython._compat import IS_PY310_OR_GREATER 7 | 8 | 9 | def test_compile_restricted_function(): 10 | p = '' 11 | body = """ 12 | print("Hello World!") 13 | return printed 14 | """ 15 | name = "hello_world" 16 | global_symbols = [] 17 | 18 | result = compile_restricted_function( 19 | p, # parameters 20 | body, 21 | name, 22 | filename='', 23 | globalize=global_symbols 24 | ) 25 | 26 | assert result.code is not None 27 | assert result.errors == () 28 | 29 | safe_globals = { 30 | '__name__': 'script', 31 | '_getattr_': getattr, 32 | '_print_': PrintCollector, 33 | '__builtins__': safe_builtins, 34 | } 35 | safe_locals = {} 36 | exec(result.code, safe_globals, safe_locals) 37 | hello_world = safe_locals['hello_world'] 38 | assert type(hello_world) is FunctionType 39 | assert hello_world() == 'Hello World!\n' 40 | 41 | 42 | def test_compile_restricted_function_func_wrapped(): 43 | p = '' 44 | body = """ 45 | print("Hello World!") 46 | return printed 47 | """ 48 | name = "hello_world" 49 | global_symbols = [] 50 | 51 | result = compile_restricted_function( 52 | p, # parameters 53 | body, 54 | name, 55 | filename='', 56 | globalize=global_symbols 57 | ) 58 | 59 | assert result.code is not None 60 | assert result.errors == () 61 | safe_globals = { 62 | '__name__': 'script', 63 | '_getattr_': getattr, 64 | '_print_': PrintCollector, 65 | '__builtins__': safe_builtins, 66 | } 67 | 68 | func = FunctionType(result.code, safe_globals) 69 | func() 70 | assert 'hello_world' in safe_globals 71 | hello_world = safe_globals['hello_world'] 72 | assert hello_world() == 'Hello World!\n' 73 | 74 | 75 | def test_compile_restricted_function_with_arguments(): 76 | p = 'input1, input2' 77 | body = """ 78 | print(input1 + input2) 79 | return printed 80 | """ 81 | name = "hello_world" 82 | global_symbols = [] 83 | 84 | result = compile_restricted_function( 85 | p, # parameters 86 | body, 87 | name, 88 | filename='', 89 | globalize=global_symbols 90 | ) 91 | 92 | assert result.code is not None 93 | assert result.errors == () 94 | 95 | safe_globals = { 96 | '__name__': 'script', 97 | '_getattr_': getattr, 98 | '_print_': PrintCollector, 99 | '__builtins__': safe_builtins, 100 | } 101 | safe_locals = {} 102 | exec(result.code, safe_globals, safe_locals) 103 | hello_world = safe_locals['hello_world'] 104 | assert type(hello_world) is FunctionType 105 | assert hello_world('Hello ', 'World!') == 'Hello World!\n' 106 | 107 | 108 | def test_compile_restricted_function_can_access_global_variables(): 109 | p = '' 110 | body = """ 111 | print(input) 112 | return printed 113 | """ 114 | name = "hello_world" 115 | global_symbols = ['input'] 116 | 117 | result = compile_restricted_function( 118 | p, # parameters 119 | body, 120 | name, 121 | filename='', 122 | globalize=global_symbols 123 | ) 124 | 125 | assert result.code is not None 126 | assert result.errors == () 127 | 128 | safe_globals = { 129 | '__name__': 'script', 130 | '_getattr_': getattr, 131 | 'input': 'Hello World!', 132 | '_print_': PrintCollector, 133 | '__builtins__': safe_builtins, 134 | } 135 | safe_locals = {} 136 | exec(result.code, safe_globals, safe_locals) 137 | hello_world = safe_locals['hello_world'] 138 | assert type(hello_world) is FunctionType 139 | assert hello_world() == 'Hello World!\n' 140 | 141 | 142 | def test_compile_restricted_function_pretends_the_code_is_executed_in_a_global_scope(): # NOQA: E501 143 | p = '' 144 | body = """output = output + 'bar'""" 145 | name = "hello_world" 146 | global_symbols = ['output'] 147 | 148 | result = compile_restricted_function( 149 | p, # parameters 150 | body, 151 | name, 152 | filename='', 153 | globalize=global_symbols 154 | ) 155 | 156 | assert result.code is not None 157 | assert result.errors == () 158 | 159 | safe_globals = { 160 | '__name__': 'script', 161 | 'output': 'foo', 162 | '__builtins__': {}, 163 | } 164 | safe_locals = {} 165 | exec(result.code, safe_globals, safe_locals) 166 | hello_world = safe_locals['hello_world'] 167 | assert type(hello_world) is FunctionType 168 | hello_world() 169 | assert safe_globals['output'] == 'foobar' 170 | 171 | 172 | def test_compile_restricted_function_allows_invalid_python_identifiers_as_function_name(): # NOQA: E501 173 | p = '' 174 | body = """output = output + 'bar'""" 175 | name = ".bar.__baz__" 176 | global_symbols = ['output'] 177 | 178 | result = compile_restricted_function( 179 | p, # parameters 180 | body, 181 | name, 182 | filename='', 183 | globalize=global_symbols 184 | ) 185 | 186 | assert result.code is not None 187 | assert result.errors == () 188 | 189 | safe_globals = { 190 | '__name__': 'script', 191 | 'output': 'foo', 192 | '__builtins__': {}, 193 | } 194 | safe_locals = {} 195 | exec(result.code, safe_globals, safe_locals) 196 | generated_function = tuple(safe_locals.values())[0] 197 | assert type(generated_function) is FunctionType 198 | generated_function() 199 | assert safe_globals['output'] == 'foobar' 200 | 201 | 202 | def test_compile_restricted_function_handle_SyntaxError(): 203 | p = '' 204 | body = """a(""" 205 | name = "broken" 206 | 207 | result = compile_restricted_function( 208 | p, # parameters 209 | body, 210 | name, 211 | ) 212 | 213 | assert result.code is None 214 | if IS_PY310_OR_GREATER: 215 | assert result.errors == ( 216 | "Line 1: SyntaxError: '(' was never closed at statement: 'a('", 217 | ) 218 | else: 219 | assert result.errors == ( 220 | "Line 1: SyntaxError: unexpected EOF while parsing at statement:" 221 | " 'a('", 222 | ) 223 | 224 | 225 | def test_compile_restricted_function_invalid_syntax(): 226 | p = '' 227 | body = '1=1' 228 | name = 'broken' 229 | 230 | result = compile_restricted_function( 231 | p, # parameters 232 | body, 233 | name, 234 | ) 235 | 236 | assert result.code is None 237 | assert len(result.errors) == 1 238 | error_msg = result.errors[0] 239 | 240 | if IS_PY310_OR_GREATER: 241 | assert error_msg.startswith( 242 | "Line 1: SyntaxError: cannot assign to literal here. Maybe " 243 | ) 244 | else: 245 | assert error_msg.startswith( 246 | "Line 1: SyntaxError: cannot assign to literal at statement:" 247 | ) 248 | -------------------------------------------------------------------------------- /docs/contributing/ast/python3_10.ast: -------------------------------------------------------------------------------- 1 | -- Python 3.10 AST 2 | -- ASDL's 4 builtin types are: 3 | -- identifier, int, string, constant 4 | 5 | module Python version "3.10" 6 | { 7 | mod = Module(stmt* body, type_ignore* type_ignores) 8 | | Interactive(stmt* body) 9 | | Expression(expr body) 10 | | FunctionType(expr* argtypes, expr returns) 11 | 12 | stmt = FunctionDef(identifier name, 13 | arguments args, 14 | stmt* body, 15 | expr* decorator_list, 16 | expr? returns, 17 | string? type_comment) 18 | | AsyncFunctionDef(identifier name, 19 | arguments args, 20 | stmt* body, 21 | expr* decorator_list, 22 | expr? returns, 23 | string? type_comment) 24 | 25 | | ClassDef(identifier name, 26 | expr* bases, 27 | keyword* keywords, 28 | stmt* body, 29 | expr* decorator_list) 30 | | Return(expr? value) 31 | 32 | | Delete(expr* targets) 33 | | Assign(expr* targets, expr value, string? type_comment) 34 | | AugAssign(expr target, operator op, expr value) 35 | -- 'simple' indicates that we annotate simple name without parens 36 | | AnnAssign(expr target, expr annotation, expr? value, int simple) 37 | 38 | -- use 'orelse' because else is a keyword in target languages 39 | | For(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) 40 | | AsyncFor(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) 41 | | While(expr test, stmt* body, stmt* orelse) 42 | | If(expr test, stmt* body, stmt* orelse) 43 | | With(withitem* items, stmt* body, string? type_comment) 44 | | AsyncWith(withitem* items, stmt* body, string? type_comment) 45 | 46 | | Match(expr subject, match_case* cases) 47 | 48 | | Raise(expr? exc, expr? cause) 49 | | Try(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody) 50 | | Assert(expr test, expr? msg) 51 | 52 | | Import(alias* names) 53 | | ImportFrom(identifier? module, alias* names, int? level) 54 | 55 | | Global(identifier* names) 56 | | Nonlocal(identifier* names) 57 | | Expr(expr value) 58 | | Pass 59 | | Break 60 | | Continue 61 | 62 | -- col_offset is the byte offset in the utf8 string the parser uses 63 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 64 | 65 | -- BoolOp() can use left & right? 66 | expr = BoolOp(boolop op, expr* values) 67 | | NamedExpr(expr target, expr value) 68 | | BinOp(expr left, operator op, expr right) 69 | | UnaryOp(unaryop op, expr operand) 70 | | Lambda(arguments args, expr body) 71 | | IfExp(expr test, expr body, expr orelse) 72 | | Dict(expr* keys, expr* values) 73 | | Set(expr* elts) 74 | | ListComp(expr elt, comprehension* generators) 75 | | SetComp(expr elt, comprehension* generators) 76 | | DictComp(expr key, expr value, comprehension* generators) 77 | | GeneratorExp(expr elt, comprehension* generators) 78 | -- the grammar constrains where yield expressions can occur 79 | | Await(expr value) 80 | | Yield(expr? value) 81 | | YieldFrom(expr value) 82 | -- need sequences for compare to distinguish between 83 | -- x < 4 < 3 and (x < 4) < 3 84 | | Compare(expr left, cmpop* ops, expr* comparators) 85 | | Call(expr func, expr* args, keyword* keywords) 86 | | FormattedValue(expr value, int conversion, expr? format_spec) 87 | | JoinedStr(expr* values) 88 | | Constant(constant value, string? kind) 89 | 90 | -- the following expression can appear in assignment context 91 | | Attribute(expr value, identifier attr, expr_context ctx) 92 | | Subscript(expr value, expr slice, expr_context ctx) 93 | | Starred(expr value, expr_context ctx) 94 | | Name(identifier id, expr_context ctx) 95 | | List(expr* elts, expr_context ctx) 96 | | Tuple(expr* elts, expr_context ctx) 97 | 98 | -- can appear only in Subscript 99 | | Slice(expr? lower, expr? upper, expr? step) 100 | 101 | -- col_offset is the byte offset in the utf8 string the parser uses 102 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 103 | 104 | expr_context = Load 105 | | Store 106 | | Del 107 | 108 | boolop = And 109 | | Or 110 | 111 | operator = Add 112 | | Sub 113 | | Mult 114 | | MatMult 115 | | Div 116 | | Mod 117 | | Pow 118 | | LShift 119 | | RShift 120 | | BitOr 121 | | BitXor 122 | | BitAnd 123 | | FloorDiv 124 | 125 | unaryop = Invert 126 | | Not 127 | | UAdd 128 | | USub 129 | 130 | cmpop = Eq 131 | | NotEq 132 | | Lt 133 | | LtE 134 | | Gt 135 | | GtE 136 | | Is 137 | | IsNot 138 | | In 139 | | NotIn 140 | 141 | comprehension = (expr target, expr iter, expr* ifs, int is_async) 142 | 143 | excepthandler = ExceptHandler(expr? type, identifier? name, stmt* body) 144 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 145 | 146 | arguments = (arg* posonlyargs, 147 | arg* args, 148 | arg? vararg, 149 | arg* kwonlyargs, 150 | expr* kw_defaults, 151 | arg? kwarg, 152 | expr* defaults) 153 | 154 | arg = (identifier arg, expr? annotation, string? type_comment) 155 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 156 | 157 | -- keyword arguments supplied to call (NULL identifier for **kwargs) 158 | keyword = (identifier? arg, expr value) 159 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 160 | 161 | -- import name with optional 'as' alias. 162 | alias = (identifier name, identifier? asname) 163 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 164 | 165 | withitem = (expr context_expr, expr? optional_vars) 166 | 167 | match_case = (pattern pattern, expr? guard, stmt* body) 168 | 169 | pattern = MatchValue(expr value) 170 | | MatchSingleton(constant value) 171 | | MatchSequence(pattern* patterns) 172 | | MatchMapping(expr* keys, pattern* patterns, identifier? rest) 173 | | MatchClass(expr cls, pattern* patterns, identifier* kwd_attrs, pattern* kwd_patterns) 174 | 175 | | MatchStar(identifier? name) 176 | -- The optional "rest" MatchMapping parameter handles capturing extra mapping keys 177 | 178 | | MatchAs(pattern? pattern, identifier? name) 179 | | MatchOr(pattern* patterns) 180 | 181 | attributes (int lineno, int col_offset, int end_lineno, int end_col_offset) 182 | 183 | type_ignore = TypeIgnore(int lineno, string tag) 184 | } 185 | -------------------------------------------------------------------------------- /docs/contributing/ast/python3_11.ast: -------------------------------------------------------------------------------- 1 | -- Python 3.11 AST 2 | -- ASDL's 4 builtin types are: 3 | -- identifier, int, string, constant 4 | 5 | module Python version "3.11" 6 | { 7 | mod = Module(stmt* body, type_ignore* type_ignores) 8 | | Interactive(stmt* body) 9 | | Expression(expr body) 10 | | FunctionType(expr* argtypes, expr returns) 11 | 12 | stmt = FunctionDef(identifier name, 13 | arguments args, 14 | stmt* body, 15 | expr* decorator_list, 16 | expr? returns, 17 | string? type_comment) 18 | | AsyncFunctionDef(identifier name, 19 | arguments args, 20 | stmt* body, 21 | expr* decorator_list, 22 | expr? returns, 23 | string? type_comment) 24 | 25 | | ClassDef(identifier name, 26 | expr* bases, 27 | keyword* keywords, 28 | stmt* body, 29 | expr* decorator_list) 30 | | Return(expr? value) 31 | 32 | | Delete(expr* targets) 33 | | Assign(expr* targets, expr value, string? type_comment) 34 | | AugAssign(expr target, operator op, expr value) 35 | -- 'simple' indicates that we annotate simple name without parens 36 | | AnnAssign(expr target, expr annotation, expr? value, int simple) 37 | 38 | -- use 'orelse' because else is a keyword in target languages 39 | | For(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) 40 | | AsyncFor(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) 41 | | While(expr test, stmt* body, stmt* orelse) 42 | | If(expr test, stmt* body, stmt* orelse) 43 | | With(withitem* items, stmt* body, string? type_comment) 44 | | AsyncWith(withitem* items, stmt* body, string? type_comment) 45 | 46 | | Match(expr subject, match_case* cases) 47 | 48 | | Raise(expr? exc, expr? cause) 49 | | Try(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody) 50 | | TryStar(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody) 51 | | Assert(expr test, expr? msg) 52 | 53 | | Import(alias* names) 54 | | ImportFrom(identifier? module, alias* names, int? level) 55 | 56 | | Global(identifier* names) 57 | | Nonlocal(identifier* names) 58 | | Expr(expr value) 59 | | Pass 60 | | Break 61 | | Continue 62 | 63 | -- col_offset is the byte offset in the utf8 string the parser uses 64 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 65 | 66 | -- BoolOp() can use left & right? 67 | expr = BoolOp(boolop op, expr* values) 68 | | NamedExpr(expr target, expr value) 69 | | BinOp(expr left, operator op, expr right) 70 | | UnaryOp(unaryop op, expr operand) 71 | | Lambda(arguments args, expr body) 72 | | IfExp(expr test, expr body, expr orelse) 73 | | Dict(expr* keys, expr* values) 74 | | Set(expr* elts) 75 | | ListComp(expr elt, comprehension* generators) 76 | | SetComp(expr elt, comprehension* generators) 77 | | DictComp(expr key, expr value, comprehension* generators) 78 | | GeneratorExp(expr elt, comprehension* generators) 79 | -- the grammar constrains where yield expressions can occur 80 | | Await(expr value) 81 | | Yield(expr? value) 82 | | YieldFrom(expr value) 83 | -- need sequences for compare to distinguish between 84 | -- x < 4 < 3 and (x < 4) < 3 85 | | Compare(expr left, cmpop* ops, expr* comparators) 86 | | Call(expr func, expr* args, keyword* keywords) 87 | | FormattedValue(expr value, int conversion, expr? format_spec) 88 | | JoinedStr(expr* values) 89 | | Constant(constant value, string? kind) 90 | 91 | -- the following expression can appear in assignment context 92 | | Attribute(expr value, identifier attr, expr_context ctx) 93 | | Subscript(expr value, expr slice, expr_context ctx) 94 | | Starred(expr value, expr_context ctx) 95 | | Name(identifier id, expr_context ctx) 96 | | List(expr* elts, expr_context ctx) 97 | | Tuple(expr* elts, expr_context ctx) 98 | 99 | -- can appear only in Subscript 100 | | Slice(expr? lower, expr? upper, expr? step) 101 | 102 | -- col_offset is the byte offset in the utf8 string the parser uses 103 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 104 | 105 | expr_context = Load 106 | | Store 107 | | Del 108 | 109 | boolop = And 110 | | Or 111 | 112 | operator = Add 113 | | Sub 114 | | Mult 115 | | MatMult 116 | | Div 117 | | Mod 118 | | Pow 119 | | LShift 120 | | RShift 121 | | BitOr 122 | | BitXor 123 | | BitAnd 124 | | FloorDiv 125 | 126 | unaryop = Invert 127 | | Not 128 | | UAdd 129 | | USub 130 | 131 | cmpop = Eq 132 | | NotEq 133 | | Lt 134 | | LtE 135 | | Gt 136 | | GtE 137 | | Is 138 | | IsNot 139 | | In 140 | | NotIn 141 | 142 | comprehension = (expr target, expr iter, expr* ifs, int is_async) 143 | 144 | excepthandler = ExceptHandler(expr? type, identifier? name, stmt* body) 145 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 146 | 147 | arguments = (arg* posonlyargs, 148 | arg* args, 149 | arg? vararg, 150 | arg* kwonlyargs, 151 | expr* kw_defaults, 152 | arg? kwarg, 153 | expr* defaults) 154 | 155 | arg = (identifier arg, expr? annotation, string? type_comment) 156 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 157 | 158 | -- keyword arguments supplied to call (NULL identifier for **kwargs) 159 | keyword = (identifier? arg, expr value) 160 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 161 | 162 | -- import name with optional 'as' alias. 163 | alias = (identifier name, identifier? asname) 164 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 165 | 166 | withitem = (expr context_expr, expr? optional_vars) 167 | 168 | match_case = (pattern pattern, expr? guard, stmt* body) 169 | 170 | pattern = MatchValue(expr value) 171 | | MatchSingleton(constant value) 172 | | MatchSequence(pattern* patterns) 173 | | MatchMapping(expr* keys, pattern* patterns, identifier? rest) 174 | | MatchClass(expr cls, pattern* patterns, identifier* kwd_attrs, pattern* kwd_patterns) 175 | 176 | | MatchStar(identifier? name) 177 | -- The optional "rest" MatchMapping parameter handles capturing extra mapping keys 178 | 179 | | MatchAs(pattern? pattern, identifier? name) 180 | | MatchOr(pattern* patterns) 181 | 182 | attributes (int lineno, int col_offset, int end_lineno, int end_col_offset) 183 | 184 | type_ignore = TypeIgnore(int lineno, string tag) 185 | } 186 | -------------------------------------------------------------------------------- /src/RestrictedPython/compile.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import warnings 3 | from collections import namedtuple 4 | 5 | from RestrictedPython._compat import IS_CPYTHON 6 | from RestrictedPython.transformer import RestrictingNodeTransformer 7 | 8 | 9 | CompileResult = namedtuple( 10 | 'CompileResult', 'code, errors, warnings, used_names') 11 | syntax_error_template = ( 12 | 'Line {lineno}: {type}: {msg} at statement: {statement!r}') 13 | 14 | NOT_CPYTHON_WARNING = ( 15 | 'RestrictedPython is only supported on CPython: use on other Python ' 16 | 'implementations may create security issues.' 17 | ) 18 | 19 | 20 | def _compile_restricted_mode( 21 | source, 22 | filename='', 23 | mode="exec", 24 | flags=0, 25 | dont_inherit=False, 26 | policy=RestrictingNodeTransformer): 27 | 28 | if not IS_CPYTHON: 29 | warnings.warn_explicit( 30 | NOT_CPYTHON_WARNING, RuntimeWarning, 'RestrictedPython', 0) 31 | 32 | byte_code = None 33 | collected_errors = [] 34 | collected_warnings = [] 35 | used_names = {} 36 | if policy is None: 37 | # Unrestricted Source Checks 38 | byte_code = compile(source, filename, mode=mode, flags=flags, 39 | dont_inherit=dont_inherit) 40 | elif issubclass(policy, RestrictingNodeTransformer): 41 | c_ast = None 42 | allowed_source_types = [str, ast.Module] 43 | if not issubclass(type(source), tuple(allowed_source_types)): 44 | raise TypeError('Not allowed source type: ' 45 | '"{0.__class__.__name__}".'.format(source)) 46 | c_ast = None 47 | # workaround for pypy issue https://bitbucket.org/pypy/pypy/issues/2552 48 | if isinstance(source, ast.Module): 49 | c_ast = source 50 | else: 51 | try: 52 | c_ast = ast.parse(source, filename, mode) 53 | except (TypeError, ValueError) as e: 54 | collected_errors.append(str(e)) 55 | except SyntaxError as v: 56 | collected_errors.append(syntax_error_template.format( 57 | lineno=v.lineno, 58 | type=v.__class__.__name__, 59 | msg=v.msg, 60 | statement=v.text.strip() if v.text else None 61 | )) 62 | if c_ast: 63 | policy_instance = policy( 64 | collected_errors, collected_warnings, used_names) 65 | policy_instance.visit(c_ast) 66 | if not collected_errors: 67 | byte_code = compile(c_ast, filename, mode=mode # , 68 | # flags=flags, 69 | # dont_inherit=dont_inherit 70 | ) 71 | else: 72 | raise TypeError('Unallowed policy provided for RestrictedPython') 73 | return CompileResult( 74 | byte_code, 75 | tuple(collected_errors), 76 | collected_warnings, 77 | used_names) 78 | 79 | 80 | def compile_restricted_exec( 81 | source, 82 | filename='', 83 | flags=0, 84 | dont_inherit=False, 85 | policy=RestrictingNodeTransformer): 86 | """Compile restricted for the mode `exec`.""" 87 | return _compile_restricted_mode( 88 | source, 89 | filename=filename, 90 | mode='exec', 91 | flags=flags, 92 | dont_inherit=dont_inherit, 93 | policy=policy) 94 | 95 | 96 | def compile_restricted_eval( 97 | source, 98 | filename='', 99 | flags=0, 100 | dont_inherit=False, 101 | policy=RestrictingNodeTransformer): 102 | """Compile restricted for the mode `eval`.""" 103 | return _compile_restricted_mode( 104 | source, 105 | filename=filename, 106 | mode='eval', 107 | flags=flags, 108 | dont_inherit=dont_inherit, 109 | policy=policy) 110 | 111 | 112 | def compile_restricted_single( 113 | source, 114 | filename='', 115 | flags=0, 116 | dont_inherit=False, 117 | policy=RestrictingNodeTransformer): 118 | """Compile restricted for the mode `single`.""" 119 | return _compile_restricted_mode( 120 | source, 121 | filename=filename, 122 | mode='single', 123 | flags=flags, 124 | dont_inherit=dont_inherit, 125 | policy=policy) 126 | 127 | 128 | def compile_restricted_function( 129 | p, # parameters 130 | body, 131 | name, 132 | filename='', 133 | globalize=None, # List of globals (e.g. ['here', 'context', ...]) 134 | flags=0, 135 | dont_inherit=False, 136 | policy=RestrictingNodeTransformer): 137 | """Compile a restricted code object for a function. 138 | 139 | Documentation see: 140 | http://restrictedpython.readthedocs.io/en/latest/usage/index.html#RestrictedPython.compile_restricted_function 141 | """ 142 | # Parse the parameters and body, then combine them. 143 | try: 144 | body_ast = ast.parse(body, '', 'exec') 145 | except SyntaxError as v: 146 | error = syntax_error_template.format( 147 | lineno=v.lineno, 148 | type=v.__class__.__name__, 149 | msg=v.msg, 150 | statement=v.text.strip() if v.text else None) 151 | return CompileResult( 152 | code=None, errors=(error,), warnings=(), used_names=()) 153 | 154 | # The compiled code is actually executed inside a function 155 | # (that is called when the code is called) so reading and assigning to a 156 | # global variable like this`printed += 'foo'` would throw an 157 | # UnboundLocalError. 158 | # We don't want the user to need to understand this. 159 | if globalize: 160 | body_ast.body.insert(0, ast.Global(globalize)) 161 | wrapper_ast = ast.parse('def masked_function_name(%s): pass' % p, 162 | '', 'exec') 163 | # In case the name you chose for your generated function is not a 164 | # valid python identifier we set it after the fact 165 | function_ast = wrapper_ast.body[0] 166 | assert isinstance(function_ast, ast.FunctionDef) 167 | function_ast.name = name 168 | 169 | wrapper_ast.body[0].body = body_ast.body 170 | wrapper_ast = ast.fix_missing_locations(wrapper_ast) 171 | 172 | result = _compile_restricted_mode( 173 | wrapper_ast, 174 | filename=filename, 175 | mode='exec', 176 | flags=flags, 177 | dont_inherit=dont_inherit, 178 | policy=policy) 179 | 180 | return result 181 | 182 | 183 | def compile_restricted( 184 | source, 185 | filename='', 186 | mode='exec', 187 | flags=0, 188 | dont_inherit=False, 189 | policy=RestrictingNodeTransformer): 190 | """Replacement for the built-in compile() function. 191 | 192 | policy ... `ast.NodeTransformer` class defining the restrictions. 193 | 194 | """ 195 | if mode in ['exec', 'eval', 'single', 'function']: 196 | result = _compile_restricted_mode( 197 | source, 198 | filename=filename, 199 | mode=mode, 200 | flags=flags, 201 | dont_inherit=dont_inherit, 202 | policy=policy) 203 | else: 204 | raise TypeError('unknown mode %s', mode) 205 | for warning in result.warnings: 206 | warnings.warn( 207 | warning, 208 | SyntaxWarning 209 | ) 210 | if result.errors: 211 | raise SyntaxError(result.errors) 212 | return result.code 213 | -------------------------------------------------------------------------------- /docs/usage/policy.rst: -------------------------------------------------------------------------------- 1 | .. _policy_builtins: 2 | 3 | Policies & builtins 4 | ------------------- 5 | 6 | RestrictedPython provides a way to define policies, by redefining restricted versions of ``print``, ``getattr``, ``setattr``, ``import``, etc.. 7 | As shortcuts it offers three stripped down versions of Python's ``__builtins__``: 8 | 9 | .. _predefined_builtins: 10 | 11 | Predefined builtins 12 | ................... 13 | 14 | ``safe_builtins`` 15 | a safe set of builtin modules and functions 16 | ``limited_builtins`` 17 | restricted sequence types (e. g. ``range``, ``list`` and ``tuple``) 18 | ``utility_builtins`` 19 | access to standard modules like math, random, string and set. 20 | 21 | ``safe_globals`` is a shortcut for ``{'__builtins__': safe_builtins}`` as this 22 | is the way globals have to be provided to the `exec` function to actually 23 | restrict the access to the builtins provided by Python. 24 | 25 | Guards 26 | ...... 27 | 28 | .. todo:: 29 | 30 | Describe Guards and predefined guard methods in details 31 | 32 | RestrictedPython predefines several guarded access and manipulation methods: 33 | 34 | * ``safer_getattr`` 35 | * ``guarded_setattr`` 36 | * ``guarded_delattr`` 37 | * ``guarded_iter_unpack_sequence`` 38 | * ``guarded_unpack_sequence`` 39 | 40 | Those and additional methods rely on a helper construct ``full_write_guard``, which is intended to help implement immutable and semi mutable objects and attributes. 41 | 42 | .. todo:: 43 | 44 | Describe full_write_guard more in detail and how it works. 45 | 46 | Implementing a policy 47 | --------------------- 48 | 49 | RestrictedPython only provides the raw material for restricted execution. 50 | To actually enforce any restrictions, you need to supply a policy 51 | implementation by providing restricted versions of ``print``, 52 | ``getattr``, ``setattr``, ``import``, etc. These restricted 53 | implementations are hooked up by providing a set of specially named 54 | objects in the global dict that you use for execution of code. 55 | Specifically: 56 | 57 | 1. ``_print_`` is a callable object that returns a handler for print 58 | statements. This handler must have a ``write()`` method that 59 | accepts a single string argument, and must return a string when 60 | called. ``RestrictedPython.PrintCollector.PrintCollector`` is a 61 | suitable implementation. 62 | 63 | 2. ``_write_`` is a guard function taking a single argument. If the 64 | object passed to it may be written to, it should be returned, 65 | otherwise the guard function should raise an exception. ``_write_`` 66 | is typically called on an object before a ``setattr`` operation. 67 | 68 | 3. ``_getattr_`` and ``_getitem_`` are guard functions, each of which 69 | takes two arguments. The first is the base object to be accessed, 70 | while the second is the attribute name or item index that will be 71 | read. The guard function should return the attribute or subitem, 72 | or raise an exception. 73 | RestrictedPython ships with a default implementation 74 | for ``_getattr_`` which prevents the following actions: 75 | 76 | * accessing an attribute whose name start with an underscore 77 | * accessing the format method of strings as this is considered harmful. 78 | 79 | 4. ``__import__`` is the normal Python import hook, and should be used 80 | to control access to Python packages and modules. 81 | 82 | 5. ``__builtins__`` is the normal Python builtins dictionary, which 83 | should be weeded down to a set that cannot be used to get around 84 | your restrictions. A usable "safe" set is 85 | ``RestrictedPython.Guards.safe_builtins``. 86 | 87 | To help illustrate how this works under the covers, here's an example 88 | function: 89 | 90 | .. code-block:: python 91 | 92 | def f(x): 93 | x.foo = x.foo + x[0] 94 | print x 95 | return printed 96 | 97 | and (sort of) how it looks after restricted compilation: 98 | 99 | .. code-block:: python 100 | 101 | def f(x): 102 | # Make local variables from globals. 103 | _print = _print_() 104 | _write = _write_ 105 | _getattr = _getattr_ 106 | _getitem = _getitem_ 107 | 108 | # Translation of f(x) above 109 | _write(x).foo = _getattr(x, 'foo') + _getitem(x, 0) 110 | print >>_print, x 111 | return _print() 112 | 113 | Examples 114 | -------- 115 | 116 | ``print`` 117 | ......... 118 | 119 | To support the ``print`` statement in restricted code, we supply a 120 | ``_print_`` object (note that it's a *factory*, e.g. a class or a 121 | callable, from which the restricted machinery will create the object): 122 | 123 | .. code-block:: pycon 124 | 125 | >>> from RestrictedPython.PrintCollector import PrintCollector 126 | >>> _print_ = PrintCollector 127 | >>> _getattr_ = getattr 128 | 129 | >>> src = ''' 130 | ... print("Hello World!") 131 | ... ''' 132 | >>> code = compile_restricted(src, '', 'exec') 133 | >>> exec(code) 134 | 135 | As you can see, the text doesn't appear on stdout. The print 136 | collector collects it. We can have access to the text using the 137 | ``printed`` variable, though: 138 | 139 | .. code-block:: pycon 140 | 141 | >>> src = ''' 142 | ... print("Hello World!") 143 | ... result = printed 144 | ... ''' 145 | >>> code = compile_restricted(src, '', 'exec') 146 | >>> exec(code) 147 | 148 | >>> result 149 | 'Hello World!\n' 150 | 151 | Built-ins 152 | ......... 153 | 154 | By supplying a different ``__builtins__`` dictionary, we can rule out 155 | unsafe operations, such as opening files: 156 | 157 | .. code-block:: pycon 158 | 159 | >>> from RestrictedPython.Guards import safe_builtins 160 | >>> restricted_globals = dict(__builtins__=safe_builtins) 161 | 162 | >>> src = ''' 163 | ... open('/etc/passwd') 164 | ... ''' 165 | >>> code = compile_restricted(src, '', 'exec') 166 | >>> exec(code, restricted_globals) 167 | Traceback (most recent call last): 168 | ... 169 | NameError: name 'open' is not defined 170 | 171 | Guards 172 | ...... 173 | 174 | Here's an example of a write guard that never lets restricted code 175 | modify (assign, delete an attribute or item) except dictionaries and 176 | lists: 177 | 178 | .. code-block:: pycon 179 | 180 | >>> from RestrictedPython.Guards import full_write_guard 181 | >>> _write_ = full_write_guard 182 | >>> _getattr_ = getattr 183 | 184 | >>> class BikeShed(object): 185 | ... colour = 'green' 186 | ... 187 | >>> shed = BikeShed() 188 | 189 | Normally accessing attributes works as expected, because we're using 190 | the standard ``getattr`` function for the ``_getattr_`` guard: 191 | 192 | .. code-block:: pycon 193 | 194 | >>> src = ''' 195 | ... print(shed.colour) 196 | ... result = printed 197 | ... ''' 198 | >>> code = compile_restricted(src, '', 'exec') 199 | >>> exec(code) 200 | 201 | >>> result 202 | 'green\n' 203 | 204 | However, changing an attribute doesn't work: 205 | 206 | .. code-block:: pycon 207 | 208 | >>> src = ''' 209 | ... shed.colour = 'red' 210 | ... ''' 211 | >>> code = compile_restricted(src, '', 'exec') 212 | >>> exec(code) 213 | Traceback (most recent call last): 214 | ... 215 | TypeError: attribute-less object (assign or del) 216 | 217 | As said, this particular write guard (``full_write_guard``) will allow 218 | restricted code to modify lists and dictionaries: 219 | 220 | .. code-block:: pycon 221 | 222 | >>> fibonacci = [1, 1, 2, 3, 4] 223 | >>> transl = dict(one=1, two=2, tres=3) 224 | >>> src = ''' 225 | ... # correct mistake in list 226 | ... fibonacci[-1] = 5 227 | ... # one item doesn't belong 228 | ... del transl['tres'] 229 | ... ''' 230 | >>> code = compile_restricted(src, '', 'exec') 231 | >>> exec(code) 232 | 233 | >>> fibonacci 234 | [1, 1, 2, 3, 5] 235 | 236 | >>> sorted(transl.keys()) 237 | ['one', 'two'] 238 | -------------------------------------------------------------------------------- /docs/contributing/ast/python3_12.ast: -------------------------------------------------------------------------------- 1 | -- Python 3.12 AST 2 | -- ASDL's 4 builtin types are: 3 | -- identifier, int, string, constant 4 | 5 | module Python version "3.12" 6 | { 7 | mod = Module(stmt* body, type_ignore* type_ignores) 8 | | Interactive(stmt* body) 9 | | Expression(expr body) 10 | | FunctionType(expr* argtypes, expr returns) 11 | 12 | stmt = FunctionDef(identifier name, 13 | arguments args, 14 | stmt* body, 15 | expr* decorator_list, 16 | expr? returns, 17 | string? type_comment, 18 | type_param* type_params) 19 | | AsyncFunctionDef(identifier name, 20 | arguments args, 21 | stmt* body, 22 | expr* decorator_list, 23 | expr? returns, 24 | string? type_comment, 25 | type_param* type_params) 26 | 27 | | ClassDef(identifier name, 28 | expr* bases, 29 | keyword* keywords, 30 | stmt* body, 31 | expr* decorator_list, 32 | type_param* type_params) 33 | | Return(expr? value) 34 | 35 | | Delete(expr* targets) 36 | | Assign(expr* targets, expr value, string? type_comment) 37 | | TypeAlias(expr name, type_param* type_params, expr value) 38 | | AugAssign(expr target, operator op, expr value) 39 | -- 'simple' indicates that we annotate simple name without parens 40 | | AnnAssign(expr target, expr annotation, expr? value, int simple) 41 | 42 | -- use 'orelse' because else is a keyword in target languages 43 | | For(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) 44 | | AsyncFor(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) 45 | | While(expr test, stmt* body, stmt* orelse) 46 | | If(expr test, stmt* body, stmt* orelse) 47 | | With(withitem* items, stmt* body, string? type_comment) 48 | | AsyncWith(withitem* items, stmt* body, string? type_comment) 49 | 50 | | Match(expr subject, match_case* cases) 51 | 52 | | Raise(expr? exc, expr? cause) 53 | | Try(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody) 54 | | TryStar(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody) 55 | | Assert(expr test, expr? msg) 56 | 57 | | Import(alias* names) 58 | | ImportFrom(identifier? module, alias* names, int? level) 59 | 60 | | Global(identifier* names) 61 | | Nonlocal(identifier* names) 62 | | Expr(expr value) 63 | | Pass 64 | | Break 65 | | Continue 66 | 67 | -- col_offset is the byte offset in the utf8 string the parser uses 68 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 69 | 70 | -- BoolOp() can use left & right? 71 | expr = BoolOp(boolop op, expr* values) 72 | | NamedExpr(expr target, expr value) 73 | | BinOp(expr left, operator op, expr right) 74 | | UnaryOp(unaryop op, expr operand) 75 | | Lambda(arguments args, expr body) 76 | | IfExp(expr test, expr body, expr orelse) 77 | | Dict(expr* keys, expr* values) 78 | | Set(expr* elts) 79 | | ListComp(expr elt, comprehension* generators) 80 | | SetComp(expr elt, comprehension* generators) 81 | | DictComp(expr key, expr value, comprehension* generators) 82 | | GeneratorExp(expr elt, comprehension* generators) 83 | -- the grammar constrains where yield expressions can occur 84 | | Await(expr value) 85 | | Yield(expr? value) 86 | | YieldFrom(expr value) 87 | -- need sequences for compare to distinguish between 88 | -- x < 4 < 3 and (x < 4) < 3 89 | | Compare(expr left, cmpop* ops, expr* comparators) 90 | | Call(expr func, expr* args, keyword* keywords) 91 | | FormattedValue(expr value, int conversion, expr? format_spec) 92 | | JoinedStr(expr* values) 93 | | Constant(constant value, string? kind) 94 | 95 | -- the following expression can appear in assignment context 96 | | Attribute(expr value, identifier attr, expr_context ctx) 97 | | Subscript(expr value, expr slice, expr_context ctx) 98 | | Starred(expr value, expr_context ctx) 99 | | Name(identifier id, expr_context ctx) 100 | | List(expr* elts, expr_context ctx) 101 | | Tuple(expr* elts, expr_context ctx) 102 | 103 | -- can appear only in Subscript 104 | | Slice(expr? lower, expr? upper, expr? step) 105 | 106 | -- col_offset is the byte offset in the utf8 string the parser uses 107 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 108 | 109 | expr_context = Load 110 | | Store 111 | | Del 112 | 113 | boolop = And 114 | | Or 115 | 116 | operator = Add 117 | | Sub 118 | | Mult 119 | | MatMult 120 | | Div 121 | | Mod 122 | | Pow 123 | | LShift 124 | | RShift 125 | | BitOr 126 | | BitXor 127 | | BitAnd 128 | | FloorDiv 129 | 130 | unaryop = Invert 131 | | Not 132 | | UAdd 133 | | USub 134 | 135 | cmpop = Eq 136 | | NotEq 137 | | Lt 138 | | LtE 139 | | Gt 140 | | GtE 141 | | Is 142 | | IsNot 143 | | In 144 | | NotIn 145 | 146 | comprehension = (expr target, expr iter, expr* ifs, int is_async) 147 | 148 | excepthandler = ExceptHandler(expr? type, identifier? name, stmt* body) 149 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 150 | 151 | arguments = (arg* posonlyargs, 152 | arg* args, 153 | arg? vararg, 154 | arg* kwonlyargs, 155 | expr* kw_defaults, 156 | arg? kwarg, 157 | expr* defaults) 158 | 159 | arg = (identifier arg, expr? annotation, string? type_comment) 160 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 161 | 162 | -- keyword arguments supplied to call (NULL identifier for **kwargs) 163 | keyword = (identifier? arg, expr value) 164 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 165 | 166 | -- import name with optional 'as' alias. 167 | alias = (identifier name, identifier? asname) 168 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 169 | 170 | withitem = (expr context_expr, expr? optional_vars) 171 | 172 | match_case = (pattern pattern, expr? guard, stmt* body) 173 | 174 | pattern = MatchValue(expr value) 175 | | MatchSingleton(constant value) 176 | | MatchSequence(pattern* patterns) 177 | | MatchMapping(expr* keys, pattern* patterns, identifier? rest) 178 | | MatchClass(expr cls, pattern* patterns, identifier* kwd_attrs, pattern* kwd_patterns) 179 | 180 | | MatchStar(identifier? name) 181 | -- The optional "rest" MatchMapping parameter handles capturing extra mapping keys 182 | 183 | | MatchAs(pattern? pattern, identifier? name) 184 | | MatchOr(pattern* patterns) 185 | 186 | attributes (int lineno, int col_offset, int end_lineno, int end_col_offset) 187 | 188 | type_ignore = TypeIgnore(int lineno, string tag) 189 | 190 | type_param = TypeVar(identifier name, expr? bound) 191 | | ParamSpec(identifier name) 192 | | TypeVarTuple(identifier name) 193 | attributes (int lineno, int col_offset, int end_lineno, int end_col_offset) 194 | } 195 | -------------------------------------------------------------------------------- /docs/contributing/ast/python3_13.ast: -------------------------------------------------------------------------------- 1 | -- Python 3.13 AST 2 | -- ASDL's 4 builtin types are: 3 | -- identifier, int, string, constant 4 | 5 | module Python version "3.13" 6 | { 7 | mod = Module(stmt* body, type_ignore* type_ignores) 8 | | Interactive(stmt* body) 9 | | Expression(expr body) 10 | | FunctionType(expr* argtypes, expr returns) 11 | 12 | stmt = FunctionDef(identifier name, 13 | arguments args, 14 | stmt* body, 15 | expr* decorator_list, 16 | expr? returns, 17 | string? type_comment, 18 | type_param* type_params) 19 | | AsyncFunctionDef(identifier name, 20 | arguments args, 21 | stmt* body, 22 | expr* decorator_list, 23 | expr? returns, 24 | string? type_comment, 25 | type_param* type_params) 26 | 27 | | ClassDef(identifier name, 28 | expr* bases, 29 | keyword* keywords, 30 | stmt* body, 31 | expr* decorator_list, 32 | type_param* type_params) 33 | | Return(expr? value) 34 | 35 | | Delete(expr* targets) 36 | | Assign(expr* targets, expr value, string? type_comment) 37 | | TypeAlias(expr name, type_param* type_params, expr value) 38 | | AugAssign(expr target, operator op, expr value) 39 | -- 'simple' indicates that we annotate simple name without parens 40 | | AnnAssign(expr target, expr annotation, expr? value, int simple) 41 | 42 | -- use 'orelse' because else is a keyword in target languages 43 | | For(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) 44 | | AsyncFor(expr target, expr iter, stmt* body, stmt* orelse, string? type_comment) 45 | | While(expr test, stmt* body, stmt* orelse) 46 | | If(expr test, stmt* body, stmt* orelse) 47 | | With(withitem* items, stmt* body, string? type_comment) 48 | | AsyncWith(withitem* items, stmt* body, string? type_comment) 49 | 50 | | Match(expr subject, match_case* cases) 51 | 52 | | Raise(expr? exc, expr? cause) 53 | | Try(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody) 54 | | TryStar(stmt* body, excepthandler* handlers, stmt* orelse, stmt* finalbody) 55 | | Assert(expr test, expr? msg) 56 | 57 | | Import(alias* names) 58 | | ImportFrom(identifier? module, alias* names, int? level) 59 | 60 | | Global(identifier* names) 61 | | Nonlocal(identifier* names) 62 | | Expr(expr value) 63 | | Pass 64 | | Break 65 | | Continue 66 | 67 | -- col_offset is the byte offset in the utf8 string the parser uses 68 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 69 | 70 | -- BoolOp() can use left & right? 71 | expr = BoolOp(boolop op, expr* values) 72 | | NamedExpr(expr target, expr value) 73 | | BinOp(expr left, operator op, expr right) 74 | | UnaryOp(unaryop op, expr operand) 75 | | Lambda(arguments args, expr body) 76 | | IfExp(expr test, expr body, expr orelse) 77 | | Dict(expr* keys, expr* values) 78 | | Set(expr* elts) 79 | | ListComp(expr elt, comprehension* generators) 80 | | SetComp(expr elt, comprehension* generators) 81 | | DictComp(expr key, expr value, comprehension* generators) 82 | | GeneratorExp(expr elt, comprehension* generators) 83 | -- the grammar constrains where yield expressions can occur 84 | | Await(expr value) 85 | | Yield(expr? value) 86 | | YieldFrom(expr value) 87 | -- need sequences for compare to distinguish between 88 | -- x < 4 < 3 and (x < 4) < 3 89 | | Compare(expr left, cmpop* ops, expr* comparators) 90 | | Call(expr func, expr* args, keyword* keywords) 91 | | FormattedValue(expr value, int conversion, expr? format_spec) 92 | | JoinedStr(expr* values) 93 | | Constant(constant value, string? kind) 94 | 95 | -- the following expression can appear in assignment context 96 | | Attribute(expr value, identifier attr, expr_context ctx) 97 | | Subscript(expr value, expr slice, expr_context ctx) 98 | | Starred(expr value, expr_context ctx) 99 | | Name(identifier id, expr_context ctx) 100 | | List(expr* elts, expr_context ctx) 101 | | Tuple(expr* elts, expr_context ctx) 102 | 103 | -- can appear only in Subscript 104 | | Slice(expr? lower, expr? upper, expr? step) 105 | 106 | -- col_offset is the byte offset in the utf8 string the parser uses 107 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 108 | 109 | expr_context = Load 110 | | Store 111 | | Del 112 | 113 | boolop = And 114 | | Or 115 | 116 | operator = Add 117 | | Sub 118 | | Mult 119 | | MatMult 120 | | Div 121 | | Mod 122 | | Pow 123 | | LShift 124 | | RShift 125 | | BitOr 126 | | BitXor 127 | | BitAnd 128 | | FloorDiv 129 | 130 | unaryop = Invert 131 | | Not 132 | | UAdd 133 | | USub 134 | 135 | cmpop = Eq 136 | | NotEq 137 | | Lt 138 | | LtE 139 | | Gt 140 | | GtE 141 | | Is 142 | | IsNot 143 | | In 144 | | NotIn 145 | 146 | comprehension = (expr target, expr iter, expr* ifs, int is_async) 147 | 148 | excepthandler = ExceptHandler(expr? type, identifier? name, stmt* body) 149 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 150 | 151 | arguments = (arg* posonlyargs, 152 | arg* args, 153 | arg? vararg, 154 | arg* kwonlyargs, 155 | expr* kw_defaults, 156 | arg? kwarg, 157 | expr* defaults) 158 | 159 | arg = (identifier arg, expr? annotation, string? type_comment) 160 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 161 | 162 | -- keyword arguments supplied to call (NULL identifier for **kwargs) 163 | keyword = (identifier? arg, expr value) 164 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 165 | 166 | -- import name with optional 'as' alias. 167 | alias = (identifier name, identifier? asname) 168 | attributes (int lineno, int col_offset, int? end_lineno, int? end_col_offset) 169 | 170 | withitem = (expr context_expr, expr? optional_vars) 171 | 172 | match_case = (pattern pattern, expr? guard, stmt* body) 173 | 174 | pattern = MatchValue(expr value) 175 | | MatchSingleton(constant value) 176 | | MatchSequence(pattern* patterns) 177 | | MatchMapping(expr* keys, pattern* patterns, identifier? rest) 178 | | MatchClass(expr cls, pattern* patterns, identifier* kwd_attrs, pattern* kwd_patterns) 179 | 180 | | MatchStar(identifier? name) 181 | -- The optional "rest" MatchMapping parameter handles capturing extra mapping keys 182 | 183 | | MatchAs(pattern? pattern, identifier? name) 184 | | MatchOr(pattern* patterns) 185 | 186 | attributes (int lineno, int col_offset, int end_lineno, int end_col_offset) 187 | 188 | type_ignore = TypeIgnore(int lineno, string tag) 189 | 190 | type_param = TypeVar(identifier name, expr? bound, expr? default_value) 191 | | ParamSpec(identifier name, expr? default_value) 192 | | TypeVarTuple(identifier name, expr? default_value) 193 | attributes (int lineno, int col_offset, int end_lineno, int end_col_offset) 194 | } 195 | --------------------------------------------------------------------------------