From 510eec3f6af816a456fe8c7ee79b2204a8d55d0a Mon Sep 17 00:00:00 2001 From: Elizabeth Esswein Date: Wed, 5 Oct 2022 16:59:47 -0400 Subject: [PATCH 1/2] change script engine to use RestrictedPython --- poetry.lock | 104 +++++++++--------- pyproject.toml | 3 +- .../services/process_instance_processor.py | 33 +++++- tests/data/dangerous-scripts/read_env.bpmn | 42 +++++++ .../dangerous-scripts/read_etc_passwd.bpmn | 42 +++++++ .../message_sender.bpmn | 2 +- .../message_sender.bpmn | 4 +- .../script_with_unit_tests.bpmn | 9 +- .../unit/test_restricted_script_engine.py | 67 +++++++++++ 9 files changed, 242 insertions(+), 64 deletions(-) create mode 100644 tests/data/dangerous-scripts/read_env.bpmn create mode 100644 tests/data/dangerous-scripts/read_etc_passwd.bpmn create mode 100644 tests/spiffworkflow_backend/unit/test_restricted_script_engine.py diff --git a/poetry.lock b/poetry.lock index 6aa985e1..f1cdb5aa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -81,10 +81,7 @@ python-versions = ">=3.7.2" [package.dependencies] lazy-object-proxy = ">=1.4.0" typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} -wrapt = [ - {version = ">=1.11,<2", markers = "python_version < \"3.11\""}, - {version = ">=1.14,<2", markers = "python_version >= \"3.11\""}, -] +wrapt = {version = ">=1.11,<2", markers = "python_version < \"3.11\""} [[package]] name = "attrs" @@ -569,7 +566,7 @@ restructuredtext-lint = "*" [[package]] name = "Flask" -version = "2.1.3" +version = "2.2.2" description = "A simple framework for building complex web applications." category = "main" optional = false @@ -580,7 +577,7 @@ click = ">=8.0" importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} itsdangerous = ">=2.0" Jinja2 = ">=3.0" -Werkzeug = ">=2.0" +Werkzeug = ">=2.2.2" [package.extras] async = ["asgiref (>=3.2)"] @@ -719,16 +716,16 @@ six = ">=1.3.0" docs = ["sphinx"] [[package]] -name = "Flask-SQLAlchemy" -version = "2.5.1" -description = "Adds SQLAlchemy support to your Flask application." +name = "flask-sqlalchemy" +version = "3.0.0" +description = "Add SQLAlchemy support to your Flask application." category = "main" optional = false -python-versions = ">= 2.7, != 3.0.*, != 3.1.*, != 3.2.*, != 3.3.*" +python-versions = ">=3.7" [package.dependencies] -Flask = ">=0.10" -SQLAlchemy = ">=0.8.0" +Flask = ">=2.2" +SQLAlchemy = ">=1.4.18" [[package]] name = "furo" @@ -796,7 +793,7 @@ tornado = ["tornado (>=0.2)"] [[package]] name = "identify" -version = "2.5.5" +version = "2.5.6" description = "File identification library for Python" category = "dev" optional = false @@ -1419,7 +1416,7 @@ pycryptodome = ["pyasn1", "pycryptodome (>=3.3.1,<4.0.0)"] [[package]] name = "python-keycloak" -version = "2.5.0" +version = "2.6.0" description = "python-keycloak is a Python package providing access to the Keycloak API." category = "main" optional = false @@ -1455,14 +1452,14 @@ tzdata = {version = "*", markers = "python_version >= \"3.6\""} [[package]] name = "pyupgrade" -version = "2.38.2" +version = "2.38.4" description = "A tool to automatically upgrade syntax for newer versions." category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -tokenize-rt = ">=3.2.0" +tokenize-rt = "<5" [[package]] name = "PyYAML" @@ -1520,6 +1517,18 @@ python-versions = "*" [package.dependencies] requests = ">=2.0.1,<3.0.0" +[[package]] +name = "RestrictedPython" +version = "5.2" +description = "RestrictedPython is a defined subset of the Python language which allows to provide a program input into a trusted environment." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.11" + +[package.extras] +docs = ["Sphinx", "sphinx-rtd-theme"] +test = ["pytest", "pytest-mock"] + [[package]] name = "restructuredtext-lint" version = "1.4.0" @@ -1567,7 +1576,7 @@ python-versions = ">=3.5" [[package]] name = "safety" -version = "2.2.0" +version = "2.2.1" description = "Checks installed dependencies for known vulnerabilities and licenses." category = "dev" optional = false @@ -1827,7 +1836,7 @@ test = ["pytest"] [[package]] name = "SpiffWorkflow" version = "1.1.7" -description = "" +description = "A workflow framework and BPMN/DMN Processor" category = "main" optional = false python-versions = "*" @@ -1845,7 +1854,7 @@ pytz = "*" type = "git" url = "https://github.com/sartography/SpiffWorkflow" reference = "main" -resolved_reference = "76947aa98d81826b88b2eefd05ebae4427b00e02" +resolved_reference = "804889ce3b993c909ea795047dd18ea0ed6e5a99" [[package]] name = "SQLAlchemy" @@ -1959,7 +1968,7 @@ test = ["mypy", "pytest", "typing-extensions"] [[package]] name = "types-pytz" -version = "2022.2.1.0" +version = "2022.4.0.0" description = "Typing stubs for pytz" category = "main" optional = false @@ -1967,7 +1976,7 @@ python-versions = "*" [[package]] name = "types-requests" -version = "2.28.11" +version = "2.28.11.1" description = "Typing stubs for requests" category = "main" optional = false @@ -2156,8 +2165,8 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" -python-versions = "^3.9" -content-hash = "7a3c07a2eef00685adbf44b6e26b740e20fc52bf85e916b6c171b13d4fcc6dc9" +python-versions = ">=3.9,<3.11" +content-hash = "1baed77013bb149fc8da553aa3826b872522d72b54bcf236f7d20e5366c20871" [metadata.files] alabaster = [ @@ -2403,8 +2412,8 @@ flake8-rst-docstrings = [ {file = "flake8_rst_docstrings-0.2.7-py3-none-any.whl", hash = "sha256:5d56075dce360bcc9c6775bfe7cb431aa395de600ca7e8d40580a28d50b2a803"}, ] Flask = [ - {file = "Flask-2.1.3-py3-none-any.whl", hash = "sha256:9013281a7402ad527f8fd56375164f3aa021ecfaff89bfe3825346c24f87e04c"}, - {file = "Flask-2.1.3.tar.gz", hash = "sha256:15972e5017df0575c3d6c090ba168b6db90259e620ac8d7ea813a396bad5b6cb"}, + {file = "Flask-2.2.2-py3-none-any.whl", hash = "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"}, + {file = "Flask-2.2.2.tar.gz", hash = "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b"}, ] Flask-Admin = [ {file = "Flask-Admin-1.6.0.tar.gz", hash = "sha256:424ffc79b7b0dfff051555686ea12e86e48dffacac14beaa319fb4502ac40988"}, @@ -2433,9 +2442,9 @@ Flask-RESTful = [ {file = "Flask-RESTful-0.3.9.tar.gz", hash = "sha256:ccec650b835d48192138c85329ae03735e6ced58e9b2d9c2146d6c84c06fa53e"}, {file = "Flask_RESTful-0.3.9-py2.py3-none-any.whl", hash = "sha256:4970c49b6488e46c520b325f54833374dc2b98e211f1b272bd4b0c516232afe2"}, ] -Flask-SQLAlchemy = [ - {file = "Flask-SQLAlchemy-2.5.1.tar.gz", hash = "sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912"}, - {file = "Flask_SQLAlchemy-2.5.1-py2.py3-none-any.whl", hash = "sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390"}, +flask-sqlalchemy = [ + {file = "Flask-SQLAlchemy-3.0.0.tar.gz", hash = "sha256:b54939fd5f48184742b7d5b222d86983e233b43140c1071a36327353e86f3b56"}, + {file = "Flask_SQLAlchemy-3.0.0-py3-none-any.whl", hash = "sha256:741dabf0903569a89e4793667e25be5bb9581e614fa0eeb81a395cc7dee40c4b"}, ] furo = [ {file = "furo-2022.9.29-py3-none-any.whl", hash = "sha256:559ee17999c0f52728481dcf6b1b0cf8c9743e68c5e3a18cb45a7992747869a9"}, @@ -2510,8 +2519,8 @@ gunicorn = [ {file = "gunicorn-20.1.0.tar.gz", hash = "sha256:e0a968b5ba15f8a328fdfd7ab1fcb5af4470c28aaf7e55df02a99bc13138e6e8"}, ] identify = [ - {file = "identify-2.5.5-py2.py3-none-any.whl", hash = "sha256:ef78c0d96098a3b5fe7720be4a97e73f439af7cf088ebf47b620aeaa10fadf97"}, - {file = "identify-2.5.5.tar.gz", hash = "sha256:322a5699daecf7c6fd60e68852f36f2ecbb6a36ff6e6e973e0d2bb6fca203ee6"}, + {file = "identify-2.5.6-py2.py3-none-any.whl", hash = "sha256:b276db7ec52d7e89f5bc4653380e33054ddc803d25875952ad90b0f012cbcdaa"}, + {file = "identify-2.5.6.tar.gz", hash = "sha256:6c32dbd747aa4ceee1df33f25fed0b0f6e0d65721b15bd151307ff7056d50245"}, ] idna = [ {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, @@ -2892,18 +2901,7 @@ py = [ {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pyasn1 = [ - {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, - {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, - {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, - {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, - {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, - {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, - {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, - {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, - {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, - {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, - {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, ] pycodestyle = [ @@ -2978,8 +2976,8 @@ python-jose = [ {file = "python_jose-3.3.0-py2.py3-none-any.whl", hash = "sha256:9b1376b023f8b298536eedd47ae1089bcdb848f1535ab30555cd92002d78923a"}, ] python-keycloak = [ - {file = "python-keycloak-2.5.0.tar.gz", hash = "sha256:b401d2c67dc1b9e2dbb3309ef2012c2d178584925dc14bd07f6bd2416e5e3ff8"}, - {file = "python_keycloak-2.5.0-py3-none-any.whl", hash = "sha256:ed1c1935ceaf5d7f928b1b3ab945130f7d54685e4b17da053dbc7bfee0c0271e"}, + {file = "python-keycloak-2.6.0.tar.gz", hash = "sha256:08c530ff86f631faccb8033d9d9345cc3148cb2cf132ff7564f025292e4dbd96"}, + {file = "python_keycloak-2.6.0-py3-none-any.whl", hash = "sha256:a1ce102b978beb56d385319b3ca20992b915c2c12d15a2d0c23f1104882f3fb6"}, ] pytz = [ {file = "pytz-2022.4-py2.py3-none-any.whl", hash = "sha256:2c0784747071402c6e99f0bafdb7da0fa22645f06554c7ae06bf6358897e9c91"}, @@ -2990,8 +2988,8 @@ pytz-deprecation-shim = [ {file = "pytz_deprecation_shim-0.1.0.post0.tar.gz", hash = "sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d"}, ] pyupgrade = [ - {file = "pyupgrade-2.38.2-py2.py3-none-any.whl", hash = "sha256:41bb9a9fd48fe57163b0dacffff433d6d5a63a0f7c2402918917b5f1a533342b"}, - {file = "pyupgrade-2.38.2.tar.gz", hash = "sha256:a5d778c9de0b53975c6a9eac2d0df5adfad244a9f7d7993d8a114223ebbda367"}, + {file = "pyupgrade-2.38.4-py2.py3-none-any.whl", hash = "sha256:944ff993c396ddc2b9012eb3de4cda138eb4c149b22c6c560d4c8bfd0e180982"}, + {file = "pyupgrade-2.38.4.tar.gz", hash = "sha256:1eb43a49f416752929741ba4d706bf3f33593d3cac9bdc217fc1ef55c047c1f4"}, ] PyYAML = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, @@ -3123,6 +3121,10 @@ requests-toolbelt = [ {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"}, {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"}, ] +RestrictedPython = [ + {file = "RestrictedPython-5.2-py2.py3-none-any.whl", hash = "sha256:fdf8621034c5dcb990a2a198f232f66b2d48866dd16d848e00ac7d187ae452ba"}, + {file = "RestrictedPython-5.2.tar.gz", hash = "sha256:634da1f6c5c122a262f433b083ee3d17a9a039f8f1b3778597efb47461cd361b"}, +] restructuredtext-lint = [ {file = "restructuredtext_lint-1.4.0.tar.gz", hash = "sha256:1b235c0c922341ab6c530390892eb9e92f90b9b75046063e047cacfb0f050c45"}, ] @@ -3167,8 +3169,8 @@ rsa = [ {file = "ruamel.yaml.clib-0.2.6.tar.gz", hash = "sha256:4ff604ce439abb20794f05613c374759ce10e3595d1867764dd1ae675b85acbd"}, ] safety = [ - {file = "safety-2.2.0-py3-none-any.whl", hash = "sha256:b1a0f4c34fb41c502a7a5c54774c18376da382bc9d866ee26b39b2c747c0de40"}, - {file = "safety-2.2.0.tar.gz", hash = "sha256:6745de12acbd60a58001fe66cb540355187d7b991b30104d9ef14ff4e4826073"}, + {file = "safety-2.2.1-py3-none-any.whl", hash = "sha256:b0049b3f0af4128834f6bc5e6cd23a20ccc28303d6c92cbc019b71f1f06bc038"}, + {file = "safety-2.2.1.tar.gz", hash = "sha256:d8b48c46ac6628bb83441b7dddc4756cfe2582abe13a112ee6e4ef1a34aad032"}, ] sentry-sdk = [ {file = "sentry-sdk-1.9.0.tar.gz", hash = "sha256:f185c53496d79b280fe5d9d21e6572aee1ab802d3354eb12314d216cfbaa8d30"}, @@ -3324,12 +3326,12 @@ typeguard = [ {file = "typeguard-2.13.3.tar.gz", hash = "sha256:00edaa8da3a133674796cf5ea87d9f4b4c367d77476e185e80251cc13dfbb8c4"}, ] types-pytz = [ - {file = "types-pytz-2022.2.1.0.tar.gz", hash = "sha256:47cfb19c52b9f75896440541db392fd312a35b279c6307a531db71152ea63e2b"}, - {file = "types_pytz-2022.2.1.0-py3-none-any.whl", hash = "sha256:50ead2254b524a3d4153bc65d00289b66898060d2938e586170dce918dbaf3b3"}, + {file = "types-pytz-2022.4.0.0.tar.gz", hash = "sha256:17d66e4b16e80ceae0787726f3a22288df7d3f9fdebeb091dc64b92c0e4ea09d"}, + {file = "types_pytz-2022.4.0.0-py3-none-any.whl", hash = "sha256:950b0f3d64ed5b03a3e29c1e38fe2be8371c933c8e97922d0352345336eb8af4"}, ] types-requests = [ - {file = "types-requests-2.28.11.tar.gz", hash = "sha256:7ee827eb8ce611b02b5117cfec5da6455365b6a575f5e3ff19f655ba603e6b4e"}, - {file = "types_requests-2.28.11-py3-none-any.whl", hash = "sha256:af5f55e803cabcfb836dad752bd6d8a0fc8ef1cd84243061c0e27dee04ccf4fd"}, + {file = "types-requests-2.28.11.1.tar.gz", hash = "sha256:02b1806c5b9904edcd87fa29236164aea0e6cdc4d93ea020cd615ef65cb43d65"}, + {file = "types_requests-2.28.11.1-py3-none-any.whl", hash = "sha256:1ff2c1301f6fe58b5d1c66cdf631ca19734cb3b1a4bbadc878d75557d183291a"}, ] types-urllib3 = [ {file = "types-urllib3-1.26.25.tar.gz", hash = "sha256:5aef0e663724eef924afa8b320b62ffef2c1736c1fa6caecfc9bc6c8ae2c3def"}, diff --git a/pyproject.toml b/pyproject.toml index 261da3f2..cd8a40f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ classifiers = [ Changelog = "https://github.com/sartography/spiffworkflow-backend/releases" [tool.poetry.dependencies] -python = "^3.9" +python = ">=3.9,<3.11" click = "^8.0.1" flask = "*" flask-admin = "*" @@ -52,6 +52,7 @@ python-keycloak = "^2.5.0" APScheduler = "^3.9.1" types-requests = "^2.28.6" Jinja2 = "^3.1.2" +RestrictedPython = "^5.2" [tool.poetry.dev-dependencies] diff --git a/src/spiffworkflow_backend/services/process_instance_processor.py b/src/spiffworkflow_backend/services/process_instance_processor.py index c5a4c635..cd8cd2bf 100644 --- a/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/src/spiffworkflow_backend/services/process_instance_processor.py @@ -3,6 +3,7 @@ import json import logging import os import time + from typing import Any from typing import Callable from typing import Dict @@ -17,9 +18,8 @@ from flask_bpmn.api.api_error import ApiError from flask_bpmn.models.db import db from lxml import etree # type: ignore from SpiffWorkflow.bpmn.exceptions import WorkflowTaskExecException # type: ignore +from SpiffWorkflow.exceptions import WorkflowException # type: ignore from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException # type: ignore -from SpiffWorkflow.bpmn.PythonScriptEngine import Box # type: ignore -from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine from SpiffWorkflow.bpmn.serializer import BpmnWorkflowSerializer # type: ignore from SpiffWorkflow.bpmn.specs.BpmnProcessSpec import BpmnProcessSpec # type: ignore from SpiffWorkflow.bpmn.specs.events import CancelEventDefinition # type: ignore @@ -27,7 +27,6 @@ from SpiffWorkflow.bpmn.specs.events import EndEvent from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore from SpiffWorkflow.dmn.serializer import BusinessRuleTaskConverter # type: ignore -from SpiffWorkflow.exceptions import WorkflowException # type: ignore from SpiffWorkflow.serializer.exceptions import MissingSpecError # type: ignore from SpiffWorkflow.spiff.parser.process import SpiffBpmnParser # type: ignore from SpiffWorkflow.spiff.serializer import BoundaryEventConverter # type: ignore @@ -75,11 +74,29 @@ from spiffworkflow_backend.services.process_model_service import ProcessModelSer from spiffworkflow_backend.services.service_task_service import ServiceTaskService from spiffworkflow_backend.services.spec_file_service import SpecFileService from spiffworkflow_backend.services.user_service import UserService +from spiffworkflow_backend.services.service_task_service import ServiceTaskService +from spiffworkflow_backend.scripts.script import Script +# Sorry about all this crap. I wanted to move this thing to another file, but +# importing a bunch of types causes circular imports. -class ProcessInstanceProcessorError(Exception): - """ProcessInstanceProcessorError.""" +from RestrictedPython import safe_globals # type: ignore +from datetime import datetime +import time +import decimal + +from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine # type: ignore +from SpiffWorkflow.bpmn.PythonScriptEngine import Box +from SpiffWorkflow.bpmn.PythonScriptEngine import DEFAULT_GLOBALS + +DEFAULT_GLOBALS.update({ + "datetime": datetime, + "time": time, + "decimal": decimal, +}) +# This will overwrite the standard builtins +DEFAULT_GLOBALS.update(safe_globals) class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore """This is a custom script processor that can be easily injected into Spiff Workflow. @@ -87,6 +104,8 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore It will execute python code read in from the bpmn. It will also make any scripts in the scripts directory available for execution. """ + def __init__(self) -> None: + super().__init__(default_globals=DEFAULT_GLOBALS) def __get_augment_methods(self, task: SpiffTask) -> Dict[str, Callable]: """__get_augment_methods.""" @@ -143,6 +162,10 @@ class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore return ServiceTaskService.scripting_additions() +class ProcessInstanceProcessorError(Exception): + """ProcessInstanceProcessorError.""" + + class MyCustomParser(BpmnDmnParser): # type: ignore """A BPMN and DMN parser that can also parse spiffworkflow-specific extensions.""" diff --git a/tests/data/dangerous-scripts/read_env.bpmn b/tests/data/dangerous-scripts/read_env.bpmn new file mode 100644 index 00000000..1c5449b5 --- /dev/null +++ b/tests/data/dangerous-scripts/read_env.bpmn @@ -0,0 +1,42 @@ + + + + + Flow_1oq5kne + + + + Flow_1r45j8e + + + + Flow_1oq5kne + Flow_1r45j8e + import os + +env = os.environ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/dangerous-scripts/read_etc_passwd.bpmn b/tests/data/dangerous-scripts/read_etc_passwd.bpmn new file mode 100644 index 00000000..40f5eda4 --- /dev/null +++ b/tests/data/dangerous-scripts/read_etc_passwd.bpmn @@ -0,0 +1,42 @@ + + + + + Flow_1oq5kne + + + + Flow_1r45j8e + + + + Flow_1oq5kne + Flow_1r45j8e + user_list = open('/etc/passwd').read() + +env = os.environ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/data/message_send_one_conversation/message_sender.bpmn b/tests/data/message_send_one_conversation/message_sender.bpmn index 74c9e9c1..a735d1ad 100644 --- a/tests/data/message_send_one_conversation/message_sender.bpmn +++ b/tests/data/message_send_one_conversation/message_sender.bpmn @@ -52,7 +52,7 @@ Flow_10conab Flow_1ihr88m - import time + timestamp = time.time() the_topica = f"first_conversation_a_{timestamp}" the_topicb = f"first_conversation_b_{timestamp}" diff --git a/tests/data/message_send_two_conversations/message_sender.bpmn b/tests/data/message_send_two_conversations/message_sender.bpmn index 338bac17..38359c92 100644 --- a/tests/data/message_send_two_conversations/message_sender.bpmn +++ b/tests/data/message_send_two_conversations/message_sender.bpmn @@ -58,7 +58,7 @@ Flow_10conab Flow_1ihr88m - import time + timestamp = time.time() topic_one_a = f"topic_one_a_conversation_{timestamp}" topic_one_b = f"topic_one_b_conversation_{timestamp}" @@ -80,7 +80,7 @@ del time Flow_0n4m9ti Flow_0q3clix - import time + timestamp = time.time() topic_two_a = f"topic_two_a_conversation_{timestamp}" topic_two_b = f"topic_two_b_conversation_{timestamp}" diff --git a/tests/data/script_with_unit_tests/script_with_unit_tests.bpmn b/tests/data/script_with_unit_tests/script_with_unit_tests.bpmn index 751ac172..0b93cf86 100644 --- a/tests/data/script_with_unit_tests/script_with_unit_tests.bpmn +++ b/tests/data/script_with_unit_tests/script_with_unit_tests.bpmn @@ -24,10 +24,11 @@ Flow_0niwe1y Flow_0htxke7 - if 'hey' in locals(): - hey = True -else: - something_else = True + try: + if not hey: + hey = True +except: + something_else = True diff --git a/tests/spiffworkflow_backend/unit/test_restricted_script_engine.py b/tests/spiffworkflow_backend/unit/test_restricted_script_engine.py new file mode 100644 index 00000000..ad5bf84c --- /dev/null +++ b/tests/spiffworkflow_backend/unit/test_restricted_script_engine.py @@ -0,0 +1,67 @@ +"""Test_various_bpmn_constructs.""" + +import pytest + +from flask.app import Flask +from tests.spiffworkflow_backend.helpers.base_test import BaseTest +from tests.spiffworkflow_backend.helpers.test_data import load_test_spec + +from spiffworkflow_backend.services.process_instance_processor import ( + ProcessInstanceProcessor, +) +from spiffworkflow_backend.services.process_instance_service import ( + ProcessInstanceService, +) + +from flask_bpmn.api.api_error import ApiError + +class TestOpenFile(BaseTest): + """TestVariousBpmnConstructs.""" + + def test_dot_notation( + self, app: Flask, with_db_and_bpmn_file_cleanup: None + ) -> None: + """Test_form_data_conversion_to_dot_dict.""" + process_model = load_test_spec( + "dangerous", + bpmn_file_name="read_etc_passwd.bpmn", + process_model_source_directory="dangerous-scripts", + ) + current_user = self.find_or_create_user() + + process_instance = self.create_process_instance_from_process_model( + process_model + ) + processor = ProcessInstanceProcessor(process_instance) + + with pytest.raises(ApiError) as exception: + processor.do_engine_steps(save=True) + assert f"name 'open' is not defined" in str( + exception.value + ) + +class TestImportModule(BaseTest): + """TestVariousBpmnConstructs.""" + + def test_dot_notation( + self, app: Flask, with_db_and_bpmn_file_cleanup: None + ) -> None: + """Test_form_data_conversion_to_dot_dict.""" + process_model = load_test_spec( + "dangerous", + bpmn_file_name="read_env.bpmn", + process_model_source_directory="dangerous-scripts", + ) + current_user = self.find_or_create_user() + + process_instance = self.create_process_instance_from_process_model( + process_model + ) + processor = ProcessInstanceProcessor(process_instance) + + with pytest.raises(ApiError) as exception: + processor.do_engine_steps(save=True) + assert f"ImportError:__import__ not found" in str( + exception.value + ) + From 11f4fd3933cb604bcbcc5918f6a375ac42c71165 Mon Sep 17 00:00:00 2001 From: burnettk Date: Wed, 5 Oct 2022 20:50:33 -0400 Subject: [PATCH 2/2] run poet pre to fix lint, and avoid updating flask and Flask-SQLAlchemy to squelch warnings --- poetry.lock | 35 +++++++++++------- pyproject.toml | 3 +- .../services/process_instance_processor.py | 36 +++++++++---------- .../unit/test_restricted_script_engine.py | 21 ++++------- 4 files changed, 48 insertions(+), 47 deletions(-) diff --git a/poetry.lock b/poetry.lock index f1cdb5aa..8b61446c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -566,7 +566,7 @@ restructuredtext-lint = "*" [[package]] name = "Flask" -version = "2.2.2" +version = "2.1.3" description = "A simple framework for building complex web applications." category = "main" optional = false @@ -577,7 +577,7 @@ click = ">=8.0" importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""} itsdangerous = ">=2.0" Jinja2 = ">=3.0" -Werkzeug = ">=2.2.2" +Werkzeug = ">=2.0" [package.extras] async = ["asgiref (>=3.2)"] @@ -717,15 +717,15 @@ docs = ["sphinx"] [[package]] name = "flask-sqlalchemy" -version = "3.0.0" -description = "Add SQLAlchemy support to your Flask application." +version = "2.5.1" +description = "Adds SQLAlchemy support to your Flask application." category = "main" optional = false -python-versions = ">=3.7" +python-versions = ">= 2.7, != 3.0.*, != 3.1.*, != 3.2.*, != 3.3.*" [package.dependencies] -Flask = ">=2.2" -SQLAlchemy = ">=1.4.18" +Flask = ">=0.10" +SQLAlchemy = ">=0.8.0" [[package]] name = "furo" @@ -2166,7 +2166,7 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>= [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.11" -content-hash = "1baed77013bb149fc8da553aa3826b872522d72b54bcf236f7d20e5366c20871" +content-hash = "f64a06b52db52800be7400b19d7ab7906a54d5b3ecc625dd2fc886e69ff775ac" [metadata.files] alabaster = [ @@ -2412,8 +2412,8 @@ flake8-rst-docstrings = [ {file = "flake8_rst_docstrings-0.2.7-py3-none-any.whl", hash = "sha256:5d56075dce360bcc9c6775bfe7cb431aa395de600ca7e8d40580a28d50b2a803"}, ] Flask = [ - {file = "Flask-2.2.2-py3-none-any.whl", hash = "sha256:b9c46cc36662a7949f34b52d8ec7bb59c0d74ba08ba6cb9ce9adc1d8676d9526"}, - {file = "Flask-2.2.2.tar.gz", hash = "sha256:642c450d19c4ad482f96729bd2a8f6d32554aa1e231f4f6b4e7e5264b16cca2b"}, + {file = "Flask-2.1.3-py3-none-any.whl", hash = "sha256:9013281a7402ad527f8fd56375164f3aa021ecfaff89bfe3825346c24f87e04c"}, + {file = "Flask-2.1.3.tar.gz", hash = "sha256:15972e5017df0575c3d6c090ba168b6db90259e620ac8d7ea813a396bad5b6cb"}, ] Flask-Admin = [ {file = "Flask-Admin-1.6.0.tar.gz", hash = "sha256:424ffc79b7b0dfff051555686ea12e86e48dffacac14beaa319fb4502ac40988"}, @@ -2443,8 +2443,8 @@ Flask-RESTful = [ {file = "Flask_RESTful-0.3.9-py2.py3-none-any.whl", hash = "sha256:4970c49b6488e46c520b325f54833374dc2b98e211f1b272bd4b0c516232afe2"}, ] flask-sqlalchemy = [ - {file = "Flask-SQLAlchemy-3.0.0.tar.gz", hash = "sha256:b54939fd5f48184742b7d5b222d86983e233b43140c1071a36327353e86f3b56"}, - {file = "Flask_SQLAlchemy-3.0.0-py3-none-any.whl", hash = "sha256:741dabf0903569a89e4793667e25be5bb9581e614fa0eeb81a395cc7dee40c4b"}, + {file = "Flask-SQLAlchemy-2.5.1.tar.gz", hash = "sha256:2bda44b43e7cacb15d4e05ff3cc1f8bc97936cc464623424102bfc2c35e95912"}, + {file = "Flask_SQLAlchemy-2.5.1-py2.py3-none-any.whl", hash = "sha256:f12c3d4cc5cc7fdcc148b9527ea05671718c3ea45d50c7e732cceb33f574b390"}, ] furo = [ {file = "furo-2022.9.29-py3-none-any.whl", hash = "sha256:559ee17999c0f52728481dcf6b1b0cf8c9743e68c5e3a18cb45a7992747869a9"}, @@ -2901,7 +2901,18 @@ py = [ {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] pyasn1 = [ + {file = "pyasn1-0.4.8-py2.4.egg", hash = "sha256:fec3e9d8e36808a28efb59b489e4528c10ad0f480e57dcc32b4de5c9d8c9fdf3"}, + {file = "pyasn1-0.4.8-py2.5.egg", hash = "sha256:0458773cfe65b153891ac249bcf1b5f8f320b7c2ce462151f8fa74de8934becf"}, + {file = "pyasn1-0.4.8-py2.6.egg", hash = "sha256:5c9414dcfede6e441f7e8f81b43b34e834731003427e5b09e4e00e3172a10f00"}, + {file = "pyasn1-0.4.8-py2.7.egg", hash = "sha256:6e7545f1a61025a4e58bb336952c5061697da694db1cae97b116e9c46abcf7c8"}, {file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"}, + {file = "pyasn1-0.4.8-py3.1.egg", hash = "sha256:78fa6da68ed2727915c4767bb386ab32cdba863caa7dbe473eaae45f9959da86"}, + {file = "pyasn1-0.4.8-py3.2.egg", hash = "sha256:08c3c53b75eaa48d71cf8c710312316392ed40899cb34710d092e96745a358b7"}, + {file = "pyasn1-0.4.8-py3.3.egg", hash = "sha256:03840c999ba71680a131cfaee6fab142e1ed9bbd9c693e285cc6aca0d555e576"}, + {file = "pyasn1-0.4.8-py3.4.egg", hash = "sha256:7ab8a544af125fb704feadb008c99a88805126fb525280b2270bb25cc1d78a12"}, + {file = "pyasn1-0.4.8-py3.5.egg", hash = "sha256:e89bf84b5437b532b0803ba5c9a5e054d21fec423a89952a74f87fa2c9b7bce2"}, + {file = "pyasn1-0.4.8-py3.6.egg", hash = "sha256:014c0e9976956a08139dc0712ae195324a75e142284d5f87f1a87ee1b068a359"}, + {file = "pyasn1-0.4.8-py3.7.egg", hash = "sha256:99fcc3c8d804d1bc6d9a099921e39d827026409a58f2a720dcdb89374ea0c776"}, {file = "pyasn1-0.4.8.tar.gz", hash = "sha256:aef77c9fb94a3ac588e87841208bdec464471d9871bd5050a287cc9a475cd0ba"}, ] pycodestyle = [ diff --git a/pyproject.toml b/pyproject.toml index cd8a40f7..c6eed332 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ Changelog = "https://github.com/sartography/spiffworkflow-backend/releases" [tool.poetry.dependencies] python = ">=3.9,<3.11" click = "^8.0.1" -flask = "*" +flask = "2.1.3" flask-admin = "*" flask-bcrypt = "*" flask-cors = "*" @@ -53,6 +53,7 @@ APScheduler = "^3.9.1" types-requests = "^2.28.6" Jinja2 = "^3.1.2" RestrictedPython = "^5.2" +Flask-SQLAlchemy = "^2.5.1" [tool.poetry.dev-dependencies] diff --git a/src/spiffworkflow_backend/services/process_instance_processor.py b/src/spiffworkflow_backend/services/process_instance_processor.py index cd8cd2bf..7f8ed7d1 100644 --- a/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/src/spiffworkflow_backend/services/process_instance_processor.py @@ -1,9 +1,10 @@ """Process_instance_processor.""" +import decimal import json import logging import os import time - +from datetime import datetime from typing import Any from typing import Callable from typing import Dict @@ -17,9 +18,12 @@ from flask import current_app from flask_bpmn.api.api_error import ApiError from flask_bpmn.models.db import db from lxml import etree # type: ignore +from RestrictedPython import safe_globals # type: ignore from SpiffWorkflow.bpmn.exceptions import WorkflowTaskExecException # type: ignore -from SpiffWorkflow.exceptions import WorkflowException # type: ignore from SpiffWorkflow.bpmn.parser.ValidationException import ValidationException # type: ignore +from SpiffWorkflow.bpmn.PythonScriptEngine import Box # type: ignore +from SpiffWorkflow.bpmn.PythonScriptEngine import DEFAULT_GLOBALS +from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine from SpiffWorkflow.bpmn.serializer import BpmnWorkflowSerializer # type: ignore from SpiffWorkflow.bpmn.specs.BpmnProcessSpec import BpmnProcessSpec # type: ignore from SpiffWorkflow.bpmn.specs.events import CancelEventDefinition # type: ignore @@ -27,6 +31,7 @@ from SpiffWorkflow.bpmn.specs.events import EndEvent from SpiffWorkflow.bpmn.workflow import BpmnWorkflow # type: ignore from SpiffWorkflow.dmn.parser.BpmnDmnParser import BpmnDmnParser # type: ignore from SpiffWorkflow.dmn.serializer import BusinessRuleTaskConverter # type: ignore +from SpiffWorkflow.exceptions import WorkflowException # type: ignore from SpiffWorkflow.serializer.exceptions import MissingSpecError # type: ignore from SpiffWorkflow.spiff.parser.process import SpiffBpmnParser # type: ignore from SpiffWorkflow.spiff.serializer import BoundaryEventConverter # type: ignore @@ -74,37 +79,30 @@ from spiffworkflow_backend.services.process_model_service import ProcessModelSer from spiffworkflow_backend.services.service_task_service import ServiceTaskService from spiffworkflow_backend.services.spec_file_service import SpecFileService from spiffworkflow_backend.services.user_service import UserService -from spiffworkflow_backend.services.service_task_service import ServiceTaskService -from spiffworkflow_backend.scripts.script import Script # Sorry about all this crap. I wanted to move this thing to another file, but # importing a bunch of types causes circular imports. -from RestrictedPython import safe_globals # type: ignore - -from datetime import datetime -import time -import decimal - -from SpiffWorkflow.bpmn.PythonScriptEngine import PythonScriptEngine # type: ignore -from SpiffWorkflow.bpmn.PythonScriptEngine import Box -from SpiffWorkflow.bpmn.PythonScriptEngine import DEFAULT_GLOBALS - -DEFAULT_GLOBALS.update({ - "datetime": datetime, - "time": time, - "decimal": decimal, -}) +DEFAULT_GLOBALS.update( + { + "datetime": datetime, + "time": time, + "decimal": decimal, + } +) # This will overwrite the standard builtins DEFAULT_GLOBALS.update(safe_globals) + class CustomBpmnScriptEngine(PythonScriptEngine): # type: ignore """This is a custom script processor that can be easily injected into Spiff Workflow. It will execute python code read in from the bpmn. It will also make any scripts in the scripts directory available for execution. """ + def __init__(self) -> None: + """__init__.""" super().__init__(default_globals=DEFAULT_GLOBALS) def __get_augment_methods(self, task: SpiffTask) -> Dict[str, Callable]: diff --git a/tests/spiffworkflow_backend/unit/test_restricted_script_engine.py b/tests/spiffworkflow_backend/unit/test_restricted_script_engine.py index ad5bf84c..a04dbbec 100644 --- a/tests/spiffworkflow_backend/unit/test_restricted_script_engine.py +++ b/tests/spiffworkflow_backend/unit/test_restricted_script_engine.py @@ -1,19 +1,14 @@ """Test_various_bpmn_constructs.""" - import pytest - from flask.app import Flask +from flask_bpmn.api.api_error import ApiError from tests.spiffworkflow_backend.helpers.base_test import BaseTest from tests.spiffworkflow_backend.helpers.test_data import load_test_spec from spiffworkflow_backend.services.process_instance_processor import ( ProcessInstanceProcessor, ) -from spiffworkflow_backend.services.process_instance_service import ( - ProcessInstanceService, -) -from flask_bpmn.api.api_error import ApiError class TestOpenFile(BaseTest): """TestVariousBpmnConstructs.""" @@ -27,7 +22,7 @@ class TestOpenFile(BaseTest): bpmn_file_name="read_etc_passwd.bpmn", process_model_source_directory="dangerous-scripts", ) - current_user = self.find_or_create_user() + self.find_or_create_user() process_instance = self.create_process_instance_from_process_model( process_model @@ -36,9 +31,8 @@ class TestOpenFile(BaseTest): with pytest.raises(ApiError) as exception: processor.do_engine_steps(save=True) - assert f"name 'open' is not defined" in str( - exception.value - ) + assert "name 'open' is not defined" in str(exception.value) + class TestImportModule(BaseTest): """TestVariousBpmnConstructs.""" @@ -52,7 +46,7 @@ class TestImportModule(BaseTest): bpmn_file_name="read_env.bpmn", process_model_source_directory="dangerous-scripts", ) - current_user = self.find_or_create_user() + self.find_or_create_user() process_instance = self.create_process_instance_from_process_model( process_model @@ -61,7 +55,4 @@ class TestImportModule(BaseTest): with pytest.raises(ApiError) as exception: processor.do_engine_steps(save=True) - assert f"ImportError:__import__ not found" in str( - exception.value - ) - + assert "ImportError:__import__ not found" in str(exception.value)