├── .codeclimate.yml ├── .coveragerc ├── .gitignore ├── .pre-commit-config.yaml ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTIONS.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── docs └── README.rst ├── examples ├── cfg_example.py ├── django.nV │ └── taskManager │ │ ├── __init__.py │ │ ├── forms.py │ │ ├── loop_false_negative.py │ │ ├── misc.py │ │ ├── models.py │ │ ├── redirect_maybe_should_trigger_vuln.py │ │ ├── settings.py │ │ ├── taskManager_urls.py │ │ ├── upload_controller.py │ │ ├── urls.py │ │ ├── views.py │ │ └── wsgi.py ├── example_inputs │ ├── assignment_multiple_assign.py │ ├── assignment_multiple_assign_call.py │ ├── assignment_starred.py │ ├── assignment_tuple_value.py │ ├── assignment_two_targets.py │ ├── assignment_with_annotation.py │ ├── assignmentandbuiltin.py │ ├── asynchronous.py │ ├── call_on_call.py │ ├── call_with_attribute.py │ ├── comprehensions.py │ ├── def_with_self_as_first_arg.py │ ├── django_flask_and_normal_functions.py │ ├── django_views.py │ ├── example.py │ ├── for_complete.py │ ├── for_func_iterator.py │ ├── for_no_orelse.py │ ├── for_tuple_target.py │ ├── function.py │ ├── function_with_multiple_return.py │ ├── function_with_params.py │ ├── generator_expression_assign.py │ ├── generator_with_multiple_yields.py │ ├── if.py │ ├── if_complete.py │ ├── if_else.py │ ├── if_else_elif.py │ ├── if_not.py │ ├── if_program.py │ ├── import.py │ ├── linear.py │ ├── list_comprehension.py │ ├── multiple_except.py │ ├── multiple_if_else.py │ ├── multiple_parameters_function.py │ ├── multiscope.py │ ├── name_constant.py │ ├── name_for.py │ ├── name_if.py │ ├── nested_if_else_elif.py │ ├── parameters_function.py │ ├── recursive.py │ ├── simple.py │ ├── simple_function.py │ ├── simple_function_with_return.py │ ├── single_comprehension.py │ ├── str_ignored.py │ ├── ternary.py │ ├── try.py │ ├── try_final.py │ ├── try_orelse.py │ ├── try_orelse_with_no_variables_to_save.py │ ├── try_orelse_with_no_variables_to_save_and_no_args.py │ ├── while.py │ ├── while_break.py │ ├── while_complete.py │ ├── while_func_comparator.py │ ├── while_func_comparator_lhs.py │ ├── while_func_comparator_rhs.py │ ├── while_no_orelse.py │ └── yield.py ├── import_test │ ├── import.py │ ├── import_ast.py │ ├── import_from.py │ ├── import_math.py │ └── import_os.py ├── import_test_project │ ├── A.py │ ├── all_folder │ │ ├── __init__.py │ │ ├── has_all.py │ │ └── no_all.py │ ├── foo │ │ └── bar.py │ ├── multiple_files │ │ ├── A.py │ │ ├── B.py │ │ ├── C.py │ │ └── D.py │ ├── other_dir │ │ ├── test_from_dot_dot.py │ │ └── test_relative_between_folders.py │ ├── package_star │ │ ├── A.py │ │ ├── B.py │ │ ├── __init__.py │ │ └── folder │ │ │ ├── C.py │ │ │ └── __init__.py │ ├── package_star_with_alias │ │ ├── A.py │ │ ├── B.py │ │ ├── __init__.py │ │ └── folder │ │ │ ├── C.py │ │ │ └── __init__.py │ ├── package_with_file │ │ ├── __init__.py │ │ └── nested_folder_without_init │ │ │ └── Starbucks.py │ ├── package_with_file_and_alias │ │ ├── __init__.py │ │ └── nested_folder_without_init │ │ │ └── Starbucks.py │ ├── package_with_folder │ │ ├── __init__.py │ │ └── nested_folder_with_init │ │ │ ├── __init__.py │ │ │ └── moose.py │ ├── package_with_folder_and_alias │ │ ├── __init__.py │ │ └── nested_folder_with_init │ │ │ ├── __init__.py │ │ │ └── moose.py │ ├── package_with_function │ │ ├── __init__.py │ │ └── nested_folder_with_init │ │ │ ├── __init__.py │ │ │ └── starbucks.py │ ├── package_with_function_and_alias │ │ ├── __init__.py │ │ └── nested_folder_with_init │ │ │ ├── __init__.py │ │ │ └── starbucks.py │ ├── test_all.py │ ├── test_from_directory.py │ ├── test_from_dot.py │ ├── test_from_file_import_star.py │ ├── test_from_package_import_star.py │ ├── test_from_package_import_star_with_alias.py │ ├── test_from_package_with_file.py │ ├── test_from_package_with_file_and_alias.py │ ├── test_from_package_with_function.py │ ├── test_from_package_with_function_and_alias.py │ ├── test_import.py │ ├── test_import_as.py │ ├── test_multiple_files_with_aliases.py │ ├── test_multiple_functions_with_aliases.py │ ├── test_no_all.py │ ├── test_package_with_file.py │ ├── test_package_with_file_and_alias.py │ ├── test_package_with_folder.py │ ├── test_package_with_folder_and_alias.py │ ├── test_package_with_function.py │ ├── test_package_with_function_and_alias.py │ ├── test_relative_from_directory.py │ ├── test_relative_level_1.py │ └── test_relative_level_2.py ├── nested_functions_code │ ├── builtin_with_user_defined_inner.py │ ├── nested_user_defined_function_calls.py │ ├── sink_with_blackbox_inner.py │ ├── sink_with_result_of_blackbox_nested.py │ ├── sink_with_result_of_user_defined_nested.py │ └── sink_with_user_defined_inner.py ├── report_cfg_examples │ ├── ast_example.py │ ├── decorator.py │ ├── for_else.py │ ├── if.py │ ├── if_else.py │ ├── if_else_elif.py │ ├── sequence.py │ ├── simple.py │ ├── small_program.py │ ├── while.py │ ├── while_break.py │ ├── while_else.py │ └── while_else_2.py ├── test_project │ ├── app.py │ ├── exceptions.py │ ├── folder │ │ ├── __init__.py │ │ ├── directory │ │ │ └── indhold.py │ │ ├── file.txt │ │ └── some.py │ ├── license.txt │ └── utils.py ├── vulnerable_code │ ├── XSS.py │ ├── XSS_assign_to_other_var.py │ ├── XSS_call.py │ ├── XSS_form.py │ ├── XSS_no_vuln.py │ ├── XSS_reassign.py │ ├── XSS_sanitised.py │ ├── XSS_url.py │ ├── XSS_variable_assign.py │ ├── XSS_variable_assign_no_vuln.py │ ├── XSS_variable_multiple_assign.py │ ├── blackbox_call_after_if.py │ ├── command_injection.py │ ├── command_injection_with_aliases.py │ ├── django_XSS.py │ ├── ensure_saved_scope.py │ ├── inter_command_injection.py │ ├── inter_command_injection_2.py │ ├── list_append.py │ ├── menu.txt │ ├── multi_chain.py │ ├── multiple_blackbox_calls_in_user_defined_call_after_if.py │ ├── multiple_nested_blackbox_calls_after_for.py │ ├── multiple_nested_user_defined_calls_after_if.py │ ├── multiple_user_defined_calls_in_blackbox_call_after_if.py │ ├── path_traversal.py │ ├── path_traversal_sanitised.py │ ├── path_traversal_sanitised_2.py │ ├── recursive.py │ ├── render_ids.py │ ├── simple_vulnerability.py │ ├── sql │ │ ├── init_db.py │ │ └── sqli.py │ ├── tainted_arg_normal_function.py │ ├── templates │ │ ├── XSS_param.html │ │ ├── attackerSite.html │ │ ├── command_injection.html │ │ ├── example2_form.html │ │ ├── example2_response.html │ │ ├── link.html │ │ └── render_ids.html │ └── yield.py └── vulnerable_code_across_files │ ├── absolute_from_file_command_injection.py │ ├── absolute_from_file_command_injection_2.py │ ├── blackbox_library_call.py │ ├── import_file_command_injection.py │ ├── import_file_command_injection_2.py │ ├── import_file_does_not_exist.py │ ├── no_false_positive_absolute_from_file_command_injection_3.py │ ├── no_false_positive_import_file_command_injection_3.py │ └── other_file.py ├── pyt ├── README.rst ├── __init__.py ├── __main__.py ├── analysis │ ├── README.rst │ ├── __init__.py │ ├── constraint_table.py │ ├── definition_chains.py │ ├── fixed_point.py │ ├── lattice.py │ └── reaching_definitions_taint.py ├── cfg │ ├── README.rst │ ├── __init__.py │ ├── alias_helper.py │ ├── expr_visitor.py │ ├── expr_visitor_helper.py │ ├── make_cfg.py │ ├── stmt_visitor.py │ └── stmt_visitor_helper.py ├── core │ ├── README.rst │ ├── __init__.py │ ├── ast_helper.py │ ├── module_definitions.py │ ├── node_types.py │ ├── project_handler.py │ └── transformer.py ├── formatters │ ├── __init__.py │ ├── json.py │ ├── screen.py │ └── text.py ├── helper_visitors │ ├── README.rst │ ├── __init__.py │ ├── call_visitor.py │ ├── label_visitor.py │ ├── right_hand_side_visitor.py │ └── vars_visitor.py ├── usage.py ├── vulnerabilities │ ├── README.rst │ ├── __init__.py │ ├── trigger_definitions_parser.py │ ├── vulnerabilities.py │ └── vulnerability_helper.py ├── vulnerability_definitions │ ├── README.rst │ ├── all_trigger_words.pyt │ ├── blackbox_mapping.json │ ├── django_trigger_words.pyt │ ├── flask_trigger_words.pyt │ ├── test_positions.pyt │ └── test_triggers.pyt └── web_frameworks │ ├── README.rst │ ├── __init__.py │ ├── framework_adaptor.py │ └── framework_helper.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── __main__.py ├── analysis │ ├── __init__.py │ ├── analysis_base_test_case.py │ └── reaching_definitions_taint_test.py ├── base_test_case.py ├── cfg │ ├── __init__.py │ ├── cfg_base_test_case.py │ ├── cfg_test.py │ ├── import_test.py │ └── nested_functions_test.py ├── core │ ├── __init__.py │ ├── project_handler_test.py │ └── transformer_test.py ├── helper_visitors │ ├── __init__.py │ ├── call_visitor_test.py │ ├── label_visitor_test.py │ └── vars_visitor_test.py ├── main_test.py ├── test_utils.py ├── usage_test.py ├── vulnerabilities │ ├── __init__.py │ ├── vulnerabilities_across_files_test.py │ ├── vulnerabilities_base_test_case.py │ └── vulnerabilities_test.py └── web_frameworks │ ├── __init__.py │ └── framework_helper_test.py └── tox.ini /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | duplication: 3 | enabled: true 4 | config: 5 | languages: 6 | - python 7 | fixme: 8 | enabled: true 9 | radon: 10 | enabled: true 11 | pep8: 12 | enabled: true 13 | checks: 14 | E501: 15 | enabled: false 16 | ratings: 17 | paths: 18 | - "pyt/*" 19 | - "**.py" 20 | exclude_paths: 21 | - "pyt/draw.py" 22 | - "pyt/github_search.py" 23 | - "pyt/intraprocedural_cfg.py" 24 | - "pyt/repo_runner.py" 25 | - "pyt/save.py" 26 | - "examples/**" 27 | - "profiling/**" 28 | - "tests/**" 29 | - "LICENSE" 30 | - "**/trigger_definitions/*" 31 | - "**.cfg" 32 | - "**.csv" 33 | - "**.in" 34 | - "**.md" 35 | - "**.png" 36 | - "**.pyt" 37 | - "**.rst" 38 | - "**.sh" 39 | - "**.txt" 40 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | show_missing = True 3 | 4 | exclude_lines = 5 | def __repr__ 6 | def __str__ 7 | if __name__ == .__main__.: 8 | pass 9 | pragma: no cover 10 | raise NotImplementedError 11 | 12 | [run] 13 | source = 14 | ./pyt 15 | ./tests 16 | omit = 17 | pyt/formatters/json.py 18 | pyt/formatters/screen.py 19 | pyt/formatters/text.py 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS X 2 | .DS_Store 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *,cover 49 | .hypothesis/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | 58 | # Sphinx documentation 59 | docs/_build/ 60 | 61 | # PyBuilder 62 | target/ 63 | 64 | #Ipython Notebook 65 | .ipynb_checkpoints 66 | *~ 67 | *# 68 | 69 | #IDE 70 | .idea/ 71 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: https://github.com/pre-commit/pre-commit-hooks 2 | sha: v0.9.1 3 | hooks: 4 | - id: trailing-whitespace 5 | - id: end-of-file-fixer 6 | - id: check-docstring-first 7 | - id: debug-statements 8 | - id: check-ast 9 | - id: check-symlinks 10 | - id: flake8 11 | args: ['--exclude=examples/*', '--ignore=E501,E741'] 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | python: 4 | - "3.6" 5 | - "3.7" 6 | install: 7 | - pip install codeclimate-test-reporter 'coverage>=4.0,<4.4' flake8 8 | before_script: 9 | # stop the build if there are Python syntax errors or undefined names 10 | - flake8 . --count --exclude=examples --select=E901,E999,F821,F822,F823 --show-source --statistics 11 | script: 12 | - coverage run -m tests 13 | - flake8 . --count --exclude=examples --max-complexity=11 --max-line-length=127 --show-source --statistics 14 | - coverage report --include=tests/* --fail-under 100 15 | - coverage report --include=pyt/* --fail-under 91 16 | after_script: 17 | - codeclimate-test-reporter 18 | -------------------------------------------------------------------------------- /CONTRIBUTIONS.md: -------------------------------------------------------------------------------- 1 | Procedure for adding new features 2 | - Pitch idea in slack 3 | - Issue created in Github 4 | - Develop the feature in a separate feature-branch 5 | - Feature branch names should start with the issues number and is allowed to contain letters and underscores, for instance: 12_add_new_awesome_feature 6 | - Remember to write unit tests for your code 7 | - Announce finished feature as pull request 8 | - Pull request reviewed by at least 1 9 | - Merge to development branch when ready 10 | - Merge into master when we all agree 11 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include pyt/vulnerability_definitions/*.pyt 2 | include pyt/vulnerability_definitions/*.json 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | test: 3 | tox 4 | -------------------------------------------------------------------------------- /docs/README.rst: -------------------------------------------------------------------------------- 1 | `Start here`_. 2 | 3 | There is also `some documentation here`_, but it might be less helpful. 4 | 5 | .. _Start here: https://github.com/python-security/pyt/tree/master/pyt 6 | .. _some documentation here: http://pyt.readthedocs.io/en/latest/?badge=latest 7 | -------------------------------------------------------------------------------- /examples/cfg_example.py: -------------------------------------------------------------------------------- 1 | from ..pyt.cfg import CFG, print_CFG, generate_ast 2 | 3 | 4 | ast = generate_ast('example_inputs/example.py') 5 | 6 | cfg = CFG() 7 | cfg.create(ast) 8 | 9 | print_CFG(cfg) 10 | -------------------------------------------------------------------------------- /examples/django.nV/taskManager/__init__.py: -------------------------------------------------------------------------------- 1 | # _ _ __ __ 2 | # __| |(_)__ _ _ _ __ _ ___ _ \ \ / / 3 | # / _` || / _` | ' \/ _` / _ \_| ' \ V / 4 | # \__,_|/ \__,_|_||_\__, \___(_)_||_\_/ 5 | # |__/ |___/ 6 | # 7 | # INSECURE APPLICATION WARNING 8 | # 9 | # django.nV is a PURPOSELY INSECURE web-application 10 | # meant to demonstrate Django security problems 11 | # UNDER NO CIRCUMSTANCES should you take any code 12 | # from django.nV for use in another web application! 13 | # 14 | -------------------------------------------------------------------------------- /examples/django.nV/taskManager/forms.py: -------------------------------------------------------------------------------- 1 | # _ _ __ __ 2 | # __| |(_)__ _ _ _ __ _ ___ _ \ \ / / 3 | # / _` || / _` | ' \/ _` / _ \_| ' \ V / 4 | # \__,_|/ \__,_|_||_\__, \___(_)_||_\_/ 5 | # |__/ |___/ 6 | # 7 | # INSECURE APPLICATION WARNING 8 | # 9 | # django.nV is a PURPOSELY INSECURE web-application 10 | # meant to demonstrate Django security problems 11 | # UNDER NO CIRCUMSTANCES should you take any code 12 | # from django.nV for use in another web application! 13 | # 14 | 15 | """ forms.py contains various Django forms for the application """ 16 | 17 | from taskManager.models import Project, Task 18 | from django import forms 19 | from django.contrib.auth.models import User 20 | 21 | 22 | def get_my_choices_users(): 23 | """ Retrieves a list of all users in the system 24 | for the user management page 25 | """ 26 | 27 | user_list = User.objects.order_by('date_joined') 28 | user_tuple = [] 29 | counter = 1 30 | for user in user_list: 31 | user_tuple.append((counter, user)) 32 | counter = counter + 1 33 | return user_tuple 34 | 35 | 36 | def get_my_choices_tasks(current_proj): 37 | """ Retrieves all tasks in the system 38 | for the task management page 39 | """ 40 | 41 | task_list = [] 42 | tasks = Task.objects.all() 43 | for task in tasks: 44 | if task.project == current_proj: 45 | task_list.append(task) 46 | 47 | task_tuple = [] 48 | counter = 1 49 | for task in task_list: 50 | task_tuple.append((counter, task)) 51 | counter = counter + 1 52 | return task_tuple 53 | 54 | 55 | def get_my_choices_projects(): 56 | """ Retrieves all projects in the system 57 | for the project management page 58 | """ 59 | 60 | proj_list = Project.objects.all() 61 | proj_tuple = [] 62 | counter = 1 63 | for proj in proj_list: 64 | proj_tuple.append((counter, proj)) 65 | counter = counter + 1 66 | return proj_tuple 67 | 68 | # A2: Broken Authentication and Session Management 69 | 70 | 71 | class UserForm(forms.ModelForm): 72 | """ User registration form """ 73 | class Meta: 74 | model = User 75 | exclude = ['groups', 'user_permissions', 'last_login', 'date_joined', 'is_active'] 76 | 77 | 78 | class ProjectFileForm(forms.Form): 79 | """ Used for uploading files attached to projects """ 80 | name = forms.CharField(max_length=300) 81 | file = forms.FileField() 82 | 83 | 84 | class ProfileForm(forms.Form): 85 | """ Provides a form for editing your own profile """ 86 | first_name = forms.CharField(max_length=30, required=False) 87 | last_name = forms.CharField(max_length=30, required=False) 88 | email = forms.CharField(max_length=300, required=False) 89 | picture = forms.FileField(required=False) 90 | -------------------------------------------------------------------------------- /examples/django.nV/taskManager/loop_false_negative.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from tempfile import NamedTemporaryFile 4 | 5 | from django.shortcuts import redirect 6 | from django.http import HttpResponse 7 | 8 | def download(request): 9 | response = HttpResponse("Hi.") 10 | fork_list = request.POST.getlist('fork_list') 11 | if request.POST and len(fork_list) > 0: 12 | tmp_file = NamedTemporaryFile() 13 | cmd = "tar -czvf %s -C %s " % (tmp_file.name,DOWNLOADS) 14 | for item in fork_list: 15 | cmd += item + " " 16 | os.system(cmd) 17 | 18 | response = HttpResponse(content_type='application/x-gzip') 19 | response['Content-Disposition'] = 'attachment; filename="%s.tar.gz"' % tmp_file.name 20 | response.write(tmp_file.file.read()) 21 | else: 22 | response = redirect("/list/") 23 | return response 24 | -------------------------------------------------------------------------------- /examples/django.nV/taskManager/misc.py: -------------------------------------------------------------------------------- 1 | # _ _ __ __ 2 | # __| |(_)__ _ _ _ __ _ ___ _ \ \ / / 3 | # / _` || / _` | ' \/ _` / _ \_| ' \ V / 4 | # \__,_|/ \__,_|_||_\__, \___(_)_||_\_/ 5 | # |__/ |___/ 6 | # 7 | # INSECURE APPLICATION WARNING 8 | # 9 | # django.nV is a PURPOSELY INSECURE web-application 10 | # meant to demonstrate Django security problems 11 | # UNDER NO CIRCUMSTANCES should you take any code 12 | # from django.nV for use in another web application! 13 | # 14 | """ misc.py contains miscellaneous functions 15 | 16 | Functions that are used in multiple places in the 17 | rest of the application, but are not tied to a 18 | specific area are stored in misc.py 19 | """ 20 | 21 | import os 22 | 23 | 24 | def store_uploaded_file(title, uploaded_file): 25 | """ Stores a temporary uploaded file on disk """ 26 | upload_dir_path = '%s/static/taskManager/uploads' % ( 27 | os.path.dirname(os.path.realpath(__file__))) 28 | if not os.path.exists(upload_dir_path): 29 | os.makedirs(upload_dir_path) 30 | 31 | # A1: Injection (shell) 32 | # Let's avoid the file corruption race condition! 33 | os.system( 34 | "mv " + 35 | uploaded_file.temporary_file_path() + 36 | " " + 37 | "%s/%s" % 38 | (upload_dir_path, 39 | title)) 40 | 41 | return '/static/taskManager/uploads/%s' % (title) 42 | -------------------------------------------------------------------------------- /examples/django.nV/taskManager/models.py: -------------------------------------------------------------------------------- 1 | # _ _ __ __ 2 | # __| |(_)__ _ _ _ __ _ ___ _ \ \ / / 3 | # / _` || / _` | ' \/ _` / _ \_| ' \ V / 4 | # \__,_|/ \__,_|_||_\__, \___(_)_||_\_/ 5 | # |__/ |___/ 6 | # 7 | # INSECURE APPLICATION WARNING 8 | # 9 | # django.nV is a PURPOSELY INSECURE web-application 10 | # meant to demonstrate Django security problems 11 | # UNDER NO CIRCUMSTANCES should you take any code 12 | # from django.nV for use in another web application! 13 | # 14 | 15 | import datetime 16 | 17 | from django.contrib.auth.models import User 18 | 19 | from django.utils import timezone 20 | from django.db import models 21 | 22 | 23 | class UserProfile(models.Model): 24 | user = models.OneToOneField(User) 25 | image = models.CharField(max_length=3000, default="") 26 | reset_token = models.CharField(max_length=7, default="") 27 | reset_token_expiration = models.DateTimeField(default=timezone.now) 28 | 29 | class Project(models.Model): 30 | title = models.CharField(max_length=50, default='Default') 31 | text = models.CharField(max_length=500) 32 | start_date = models.DateTimeField('date started') 33 | due_date = models.DateTimeField( 34 | 'date due', 35 | default=( 36 | timezone.now() + 37 | datetime.timedelta( 38 | weeks=1))) 39 | users_assigned = models.ManyToManyField(User) 40 | priority = models.IntegerField(default=1) 41 | 42 | def __str__(self): 43 | return self.title 44 | 45 | def was_created_recently(self): 46 | return self.start_date >= timezone.now() - datetime.timedelta(days=1) 47 | 48 | def is_overdue(self): 49 | return self.due_date <= timezone.now() 50 | 51 | def percent_complete(self): 52 | counter = 0 53 | for task in self.task_set.all(): 54 | counter = counter + (1 if task.completed else 0) 55 | try: 56 | return round(float(counter) / self.task_set.count() * 100) 57 | except ZeroDivisionError: 58 | return 0 59 | 60 | 61 | class Task(models.Model): 62 | project = models.ForeignKey(Project, default=1) 63 | text = models.CharField(max_length=200) 64 | title = models.CharField(max_length=200, default="N/A") 65 | start_date = models.DateTimeField('date created') 66 | due_date = models.DateTimeField( 67 | 'date due', 68 | default=( 69 | timezone.now() + 70 | datetime.timedelta( 71 | weeks=1))) 72 | completed = models.NullBooleanField(default=False) 73 | users_assigned = models.ManyToManyField(User) 74 | 75 | def __str__(self): 76 | return self.text 77 | 78 | def was_created_recently(self): 79 | return self.start_date >= timezone.now() - datetime.timedelta(days=1) 80 | 81 | def is_overdue(self): 82 | return self.due_date <= timezone.now() 83 | 84 | def percent_complete(self): 85 | return 100 if self.completed else 0 86 | 87 | 88 | class Notes(models.Model): 89 | task = models.ForeignKey(Task, default=1) 90 | title = models.CharField(max_length=200, default="N/A") 91 | text = models.CharField(max_length=200) 92 | image = models.CharField(max_length=200) 93 | user = models.CharField(max_length=200, default='ancestor') 94 | 95 | def __str__(self): 96 | return self.text 97 | 98 | 99 | class File(models.Model): 100 | project = models.ForeignKey(Project) 101 | name = models.CharField(max_length=300, default="") 102 | path = models.CharField(max_length=3000, default="") 103 | 104 | def __str__(self): 105 | return self.name 106 | -------------------------------------------------------------------------------- /examples/django.nV/taskManager/redirect_maybe_should_trigger_vuln.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, render_to_response, redirect 2 | 3 | 4 | def task_edit(request, project_id, task_id): 5 | 6 | proj = Project.objects.get(pk=project_id) 7 | task = Task.objects.get(pk=task_id) 8 | 9 | if request.method == 'POST': 10 | 11 | if task.project == proj: 12 | 13 | text = request.POST.get('text', False) 14 | task_title = request.POST.get('task_title', False) 15 | task_completed = request.POST.get('task_completed', False) 16 | 17 | task.title = task_title 18 | task.text = text 19 | task.completed = True if task_completed == "1" else False 20 | task.save() 21 | 22 | return redirect('/taskManager/' + project_id + '/' + task_id) 23 | else: 24 | return render_to_response( 25 | 'taskManager/task_edit.html', {'task': task}, RequestContext(request)) 26 | -------------------------------------------------------------------------------- /examples/django.nV/taskManager/settings.py: -------------------------------------------------------------------------------- 1 | # _ _ __ __ 2 | # __| |(_)__ _ _ _ __ _ ___ _ \ \ / / 3 | # / _` || / _` | ' \/ _` / _ \_| ' \ V / 4 | # \__,_|/ \__,_|_||_\__, \___(_)_||_\_/ 5 | # |__/ |___/ 6 | # 7 | # INSECURE APPLICATION WARNING 8 | # 9 | # django.nV is a PURPOSELY INSECURE web-application 10 | # meant to demonstrate Django security problems 11 | # UNDER NO CIRCUMSTANCES should you take any code 12 | # from django.nV for use in another web application! 13 | # 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | import os 17 | BASE_DIR = os.path.dirname(os.path.dirname(__file__)) 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = '0yxzudryd8)-%)(fz&7q-!v&cq1u6vbfoc4u7@u_&i)b@4eh^q' 25 | 26 | # A5: Security Misconfiguration 27 | # SECURITY WARNING: don't run with debug turned on in production! 28 | DEBUG = True 29 | TEMPLATE_DEBUG = True 30 | 31 | ALLOWED_HOSTS = [] 32 | 33 | # Application definition 34 | 35 | INSTALLED_APPS = ( 36 | 'django.contrib.admin', 37 | 'django.contrib.auth', 38 | 'django.contrib.contenttypes', 39 | 'django.contrib.messages', 40 | 'django.contrib.staticfiles', 41 | 'taskManager' 42 | ) 43 | 44 | MIDDLEWARE_CLASSES = ( 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | ) 53 | 54 | ROOT_URLCONF = 'taskManager.urls' 55 | 56 | WSGI_APPLICATION = 'taskManager.wsgi.application' 57 | 58 | FILE_UPLOAD_HANDLERS = ( 59 | "django.core.files.uploadhandler.TemporaryFileUploadHandler",) 60 | 61 | # Database 62 | # https://docs.djangoproject.com/en/1.7/ref/settings/#databases 63 | 64 | DATABASES = { 65 | 'default': { 66 | 'ENGINE': 'django.db.backends.sqlite3', 67 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 68 | } 69 | } 70 | 71 | # Internationalization 72 | # https://docs.djangoproject.com/en/1.7/topics/i18n/ 73 | 74 | LANGUAGE_CODE = 'en-us' 75 | 76 | TIME_ZONE = 'UTC' 77 | 78 | USE_I18N = True 79 | 80 | USE_L10N = True 81 | 82 | USE_TZ = True 83 | 84 | MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage' 85 | 86 | # Static files (CSS, JavaScript, Images) 87 | # https://docs.djangoproject.com/en/1.7/howto/static-files/ 88 | 89 | STATIC_URL = '/static/' 90 | 91 | STATICFILES_DIRS = ( 92 | os.path.join(BASE_DIR, "static"), 93 | '/var/www/static/', 94 | ) 95 | 96 | TEMPLATE_DIRS = [os.path.join(BASE_DIR, 'templates')] 97 | 98 | LOGIN_URL = '/taskManager/login/' 99 | 100 | # A6: Sensitive Data Exposure 101 | PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher'] 102 | 103 | # A2: Broken Auth and Session Management 104 | SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies" 105 | 106 | EMAIL_PORT = 1025 107 | 108 | # Needs compatibility with older Django! 109 | SESSION_SERIALIZER = "django.contrib.sessions.serializers.PickleSerializer" 110 | SESSION_COOKIE_HTTPONLY = False 111 | -------------------------------------------------------------------------------- /examples/django.nV/taskManager/taskManager_urls.py: -------------------------------------------------------------------------------- 1 | # _ _ __ __ 2 | # __| |(_)__ _ _ _ __ _ ___ _ \ \ / / 3 | # / _` || / _` | ' \/ _` / _ \_| ' \ V / 4 | # \__,_|/ \__,_|_||_\__, \___(_)_||_\_/ 5 | # |__/ |___/ 6 | # 7 | # INSECURE APPLICATION WARNING 8 | # 9 | # django.nV is a PURPOSELY INSECURE web-application 10 | # meant to demonstrate Django security problems 11 | # UNDER NO CIRCUMSTANCES should you take any code 12 | # from django.nV for use in another web application! 13 | # 14 | 15 | from django.conf.urls import patterns, url 16 | 17 | from taskManager import views 18 | 19 | urlpatterns = patterns('', 20 | url(r'^$', views.index, name='index'), 21 | 22 | # File 23 | url(r'^download/(?P\d+)/$', 24 | views.download, name='download'), 25 | url(r'^(?P\d+)/upload/$', 26 | views.upload, name='upload'), 27 | url(r'^downloadprofilepic/(?P\d+)/$', 28 | views.download_profile_pic, name='download_profile_pic'), 29 | 30 | # Authentication & Authorization 31 | url(r'^register/$', views.register, name='register'), 32 | url(r'^login/$', views.login, name='login'), 33 | url(r'^logout/$', views.logout_view, name='logout'), 34 | url(r'^manage_groups/$', views.manage_groups, 35 | name='manage_groups'), 36 | url(r'^profile/$', views.profile, name='profile'), 37 | url(r'^change_password/$', views.change_password, 38 | name='change_password'), 39 | url(r'^forgot_password/$', views.forgot_password, 40 | name='forgot_password'), 41 | url(r'^reset_password/$', views.reset_password, 42 | name='reset_password'), 43 | url(r'^profile/(?P\d+)$', 44 | views.profile_by_id, name='profile_by_id'), 45 | url(r'^profile_view/(?P\d+)$', 46 | views.profile_view, name='profile_view'), 47 | 48 | # Projects 49 | url(r'^project_create/$', views.project_create, 50 | name='project_create'), 51 | url(r'^(?P\d+)/edit_project/$', 52 | views.project_edit, name='project_edit'), 53 | url(r'^manage_projects/$', views.manage_projects, 54 | name='manage_projects'), 55 | url(r'^(?P\d+)/project_delete/$', 56 | views.project_delete, name='project_delete'), 57 | url(r'^(?P\d+)/$', 58 | views.project_details, name='project_details'), 59 | url(r'^project_list/$', views.project_list, 60 | name='project_list'), 61 | 62 | # Tasks 63 | url(r'^(?P\d+)/task_create/$', 64 | views.task_create, name='task_create'), 65 | url(r'^(?P\d+)/(?P\d+)/$', 66 | views.task_details, name='task_details'), 67 | url(r'^(?P\d+)/task_edit/(?P\d+)$', 68 | views.task_edit, name='task_edit'), 69 | url(r'^(?P\d+)/task_delete/(?P\d+)$', 70 | views.task_delete, name='task_delete'), 71 | url(r'^(?P\d+)/task_complete/(?P\d+)$', 72 | views.task_complete, name='task_complete'), 73 | url(r'^task_list/$', views.task_list, name='task_list'), 74 | url(r'^(?P\d+)/manage_tasks/$', 75 | views.manage_tasks, name='manage_tasks'), 76 | 77 | 78 | # Notes 79 | url(r'^(?P\d+)/(?P\d+)/note_create/$', 80 | views.note_create, name='note_create'), 81 | url(r'^(?P\d+)/(?P\d+)/note_edit/(?P\d+)$', 82 | views.note_edit, name='note_edit'), 83 | url(r'^(?P\d+)/(?P\d+)/note_delete/(?P\d+)$', 84 | views.note_delete, name='note_delete'), 85 | 86 | url(r'^dashboard/$', views.dashboard, name='dashboard'), 87 | url(r'^search/$', views.search, name='search'), 88 | 89 | 90 | # Tutorials 91 | url(r'^tutorials/$', views.tutorials, name='tutorials'), 92 | url(r'^tutorials/(?P[a-z\-]+)/$', 93 | views.show_tutorial, name='show_tutorial'), 94 | 95 | # Settings - DEBUG 96 | url(r'^settings/$', views.tm_settings, name='settings'), 97 | ) 98 | -------------------------------------------------------------------------------- /examples/django.nV/taskManager/upload_controller.py: -------------------------------------------------------------------------------- 1 | from taskManager.misc import store_uploaded_file 2 | 3 | def upload(request, project_id): 4 | 5 | if request.method == 'POST': 6 | 7 | proj = Project.objects.get(pk=project_id) 8 | form = ProjectFileForm(request.POST, request.FILES) 9 | 10 | if form.is_valid(): 11 | name = request.POST.get('name', False) 12 | upload_path = store_uploaded_file(name, request.FILES['file']) 13 | 14 | #A1 - Injection (SQLi) 15 | curs = connection.cursor() 16 | curs.execute( 17 | "insert into taskManager_file ('name','path','project_id') values ('%s','%s',%s)" % 18 | (name, upload_path, project_id)) 19 | 20 | return redirect('/taskManager/' + project_id + 21 | '/', {'new_file_added': True}) 22 | else: 23 | form = ProjectFileForm() 24 | else: 25 | form = ProjectFileForm() 26 | return render_to_response( 27 | 'taskManager/upload.html', {'form': form}, RequestContext(request)) 28 | -------------------------------------------------------------------------------- /examples/django.nV/taskManager/urls.py: -------------------------------------------------------------------------------- 1 | # _ _ __ __ 2 | # __| |(_)__ _ _ _ __ _ ___ _ \ \ / / 3 | # / _` || / _` | ' \/ _` / _ \_| ' \ V / 4 | # \__,_|/ \__,_|_||_\__, \___(_)_||_\_/ 5 | # |__/ |___/ 6 | # 7 | # INSECURE APPLICATION WARNING 8 | # 9 | # django.nV is a PURPOSELY INSECURE web-application 10 | # meant to demonstrate Django security problems 11 | # UNDER NO CIRCUMSTANCES should you take any code 12 | # from django.nV for use in another web application! 13 | # 14 | 15 | from django.conf.urls import patterns, include, url 16 | from django.contrib import admin 17 | 18 | urlpatterns = patterns('', 19 | url(r'^$', 20 | 'taskManager.views.index', 21 | name='index'), 22 | url(r'^taskManager/', 23 | include('taskManager.taskManager_urls', 24 | namespace="taskManager")), 25 | url(r'^admin/', 26 | include(admin.site.urls)), 27 | ) 28 | -------------------------------------------------------------------------------- /examples/django.nV/taskManager/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for projmanager project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "taskManager.settings") 12 | 13 | from django.core.wsgi import get_wsgi_application 14 | application = get_wsgi_application() 15 | -------------------------------------------------------------------------------- /examples/example_inputs/assignment_multiple_assign.py: -------------------------------------------------------------------------------- 1 | y = x = 5 2 | -------------------------------------------------------------------------------- /examples/example_inputs/assignment_multiple_assign_call.py: -------------------------------------------------------------------------------- 1 | x, y = int(5), int(4) 2 | -------------------------------------------------------------------------------- /examples/example_inputs/assignment_starred.py: -------------------------------------------------------------------------------- 1 | a, *b, c, d, e = f, *g, *h, f + i, j 2 | -------------------------------------------------------------------------------- /examples/example_inputs/assignment_tuple_value.py: -------------------------------------------------------------------------------- 1 | a = x, y 2 | -------------------------------------------------------------------------------- /examples/example_inputs/assignment_two_targets.py: -------------------------------------------------------------------------------- 1 | x,y = 1,2 2 | -------------------------------------------------------------------------------- /examples/example_inputs/assignment_with_annotation.py: -------------------------------------------------------------------------------- 1 | x: int 2 | y: int=5 3 | -------------------------------------------------------------------------------- /examples/example_inputs/assignmentandbuiltin.py: -------------------------------------------------------------------------------- 1 | x = 1 2 | print(x) 3 | -------------------------------------------------------------------------------- /examples/example_inputs/asynchronous.py: -------------------------------------------------------------------------------- 1 | async def g(x, *args): 2 | return await x() 3 | 4 | 5 | async def f(y): 6 | z = await g(y, await v) 7 | return z 8 | 9 | 10 | f(w) 11 | -------------------------------------------------------------------------------- /examples/example_inputs/call_on_call.py: -------------------------------------------------------------------------------- 1 | class String(): 2 | def op(self, b): 3 | return String() 4 | 5 | def operation(self, a): 6 | return a 7 | 8 | obj = String() 9 | s = obj.op('x').operation('g') 10 | -------------------------------------------------------------------------------- /examples/example_inputs/call_with_attribute.py: -------------------------------------------------------------------------------- 1 | def foo(en): 2 | return en 3 | 4 | x = 5 5 | request.args.get('param', 'not set') 6 | y = foo(x) 7 | c = 6 8 | -------------------------------------------------------------------------------- /examples/example_inputs/comprehensions.py: -------------------------------------------------------------------------------- 1 | l = [x for x in [1,2,3]] 2 | ll = [(x, y) for x in [1,2,3] for y in [4,5,6]] 3 | d = {i : x for i,x in enumerate([1,2,3])} 4 | s = {x for x in [1,2,3,2,2,1,2]} 5 | g = (x for x in [1,2,3]) 6 | dd = { x + y : y for x in [1,2,3] for y in [4,5,6]} 7 | -------------------------------------------------------------------------------- /examples/example_inputs/def_with_self_as_first_arg.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def my_data(self, foo, bar): 4 | return redirect(self.something) 5 | -------------------------------------------------------------------------------- /examples/example_inputs/django_flask_and_normal_functions.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | print('h') 3 | 4 | def _hidden_foo(): 5 | print('h') 6 | 7 | @app.route('/', methods = ['GET']) 8 | def flask_function(x): 9 | return x 10 | 11 | def django_function(request, x): 12 | return x 13 | 14 | print('nothing') 15 | -------------------------------------------------------------------------------- /examples/example_inputs/django_views.py: -------------------------------------------------------------------------------- 1 | def django_view_function(request, x): 2 | return x 3 | 4 | 5 | class DjangoViewClass(object): 6 | def __init__(self): 7 | pass 8 | 9 | @classmethod 10 | def as_view(cls): 11 | def view_function(request, x): 12 | return x 13 | return view_function 14 | 15 | 16 | # in practice, this would be called in a Django URLconf file 17 | view = DjangoViewClass.as_view() 18 | -------------------------------------------------------------------------------- /examples/example_inputs/example.py: -------------------------------------------------------------------------------- 1 | x = input() 2 | x = int(x) 3 | while x > 1: 4 | y = x / 2 5 | if y > 3: 6 | x = x-y 7 | z = x-4 8 | if z > 0: 9 | x = x / 2 10 | z = z - 1 11 | print(x) 12 | # exit 13 | -------------------------------------------------------------------------------- /examples/example_inputs/for_complete.py: -------------------------------------------------------------------------------- 1 | for x in range(3): 2 | print(x) 3 | y += 1 4 | else: 5 | print('Final: %s' % x) 6 | print(y) 7 | x = 3 8 | -------------------------------------------------------------------------------- /examples/example_inputs/for_func_iterator.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | return range(1, 8) 3 | 4 | for x in foo(): 5 | print(x) 6 | -------------------------------------------------------------------------------- /examples/example_inputs/for_no_orelse.py: -------------------------------------------------------------------------------- 1 | for x in range(3): 2 | print(x) 3 | y += 1 4 | x = 3 5 | -------------------------------------------------------------------------------- /examples/example_inputs/for_tuple_target.py: -------------------------------------------------------------------------------- 1 | for x,y in [(1,2), (3,4)]: 2 | print(x,y) 3 | -------------------------------------------------------------------------------- /examples/example_inputs/function.py: -------------------------------------------------------------------------------- 1 | # import random 2 | 3 | def foo(): 4 | print('h') 5 | 6 | def bar(x): 7 | y = input() 8 | print(y) 9 | print(x) 10 | 11 | def baz(x): 12 | x = 1 13 | return x+1 14 | 15 | y = input() 16 | foo() 17 | #bar(3) 18 | #x = baz(1) 19 | #print(y) 20 | -------------------------------------------------------------------------------- /examples/example_inputs/function_with_multiple_return.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | a = 1 3 | if a == 1: 4 | return 0 5 | return a 6 | 7 | foo() 8 | -------------------------------------------------------------------------------- /examples/example_inputs/function_with_params.py: -------------------------------------------------------------------------------- 1 | def foo(x): 2 | z = 1 3 | x.append(z) 4 | 5 | l = [1,2,3] 6 | foo(l) 7 | -------------------------------------------------------------------------------- /examples/example_inputs/generator_expression_assign.py: -------------------------------------------------------------------------------- 1 | x = ''.join(x.n for x in range(16)) 2 | -------------------------------------------------------------------------------- /examples/example_inputs/generator_with_multiple_yields.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | a = 1 3 | if a == 1: 4 | yield 0 5 | yield a 6 | 7 | foo() 8 | -------------------------------------------------------------------------------- /examples/example_inputs/if.py: -------------------------------------------------------------------------------- 1 | if True: 2 | x = 0 3 | -------------------------------------------------------------------------------- /examples/example_inputs/if_complete.py: -------------------------------------------------------------------------------- 1 | if x > 0: 2 | x += 1 3 | x += 2 4 | elif x == 0: 5 | x += 3 6 | else: 7 | x += 4 8 | x += 5 9 | -------------------------------------------------------------------------------- /examples/example_inputs/if_else.py: -------------------------------------------------------------------------------- 1 | if True: 2 | x = 0 3 | else: 4 | y = 0 5 | -------------------------------------------------------------------------------- /examples/example_inputs/if_else_elif.py: -------------------------------------------------------------------------------- 1 | if True: 2 | x = 0 3 | elif False: 4 | y = 0 5 | else: 6 | z = 0 7 | -------------------------------------------------------------------------------- /examples/example_inputs/if_not.py: -------------------------------------------------------------------------------- 1 | if not x > 0: 2 | pass 3 | -------------------------------------------------------------------------------- /examples/example_inputs/if_program.py: -------------------------------------------------------------------------------- 1 | x = input() 2 | if x > 0: 3 | y = x + 1 4 | print(x) 5 | -------------------------------------------------------------------------------- /examples/example_inputs/import.py: -------------------------------------------------------------------------------- 1 | import import_os 2 | import import_ast as a 3 | import import_math as _ 4 | -------------------------------------------------------------------------------- /examples/example_inputs/linear.py: -------------------------------------------------------------------------------- 1 | x = input() 2 | y = x - 1 3 | print(x) 4 | -------------------------------------------------------------------------------- /examples/example_inputs/list_comprehension.py: -------------------------------------------------------------------------------- 1 | x = ''.join(x.n for x in range(16)) 2 | -------------------------------------------------------------------------------- /examples/example_inputs/multiple_except.py: -------------------------------------------------------------------------------- 1 | try: 2 | value = None 3 | except ImportError: 4 | value = 1 5 | value = 2 6 | except AttributeError: 7 | value = 3 8 | except: 9 | value = 4 10 | -------------------------------------------------------------------------------- /examples/example_inputs/multiple_if_else.py: -------------------------------------------------------------------------------- 1 | if True: 2 | x = 0 3 | else: 4 | y = 0 5 | if True: 6 | z = 0 7 | if False: 8 | k = 0 9 | -------------------------------------------------------------------------------- /examples/example_inputs/multiple_parameters_function.py: -------------------------------------------------------------------------------- 1 | def foo(a, b, c, x): 2 | print(a) 3 | print(b) 4 | return c 5 | 6 | y = 0 7 | x = foo(1, 0, 2, 3) 8 | z = 0 9 | -------------------------------------------------------------------------------- /examples/example_inputs/multiscope.py: -------------------------------------------------------------------------------- 1 | y = 1 2 | for x in range(5): 3 | g = 10 4 | if x > y: 5 | while y < 5: 6 | print( x) 7 | x += 1 8 | else: 9 | print(y + x) 10 | else: 11 | print(x*2) 12 | h = g * 2 13 | 14 | print(x) 15 | -------------------------------------------------------------------------------- /examples/example_inputs/name_constant.py: -------------------------------------------------------------------------------- 1 | x = True 2 | if True: 3 | y = 0 4 | else: 5 | y = 1 6 | -------------------------------------------------------------------------------- /examples/example_inputs/name_for.py: -------------------------------------------------------------------------------- 1 | for x in l: 2 | print(x) 3 | -------------------------------------------------------------------------------- /examples/example_inputs/name_if.py: -------------------------------------------------------------------------------- 1 | x = 1 2 | if x: 3 | y = 1 4 | -------------------------------------------------------------------------------- /examples/example_inputs/nested_if_else_elif.py: -------------------------------------------------------------------------------- 1 | if True: 2 | y = 0 3 | if False: 4 | x = 1 5 | elif True: 6 | x = 2 7 | else: 8 | x = 3 9 | elif False: 10 | y = 1 11 | else: 12 | y = 2 13 | -------------------------------------------------------------------------------- /examples/example_inputs/parameters_function.py: -------------------------------------------------------------------------------- 1 | def bar(x): 2 | y = input() 3 | print(y) 4 | print(x) 5 | 6 | y = input() 7 | bar(y) 8 | -------------------------------------------------------------------------------- /examples/example_inputs/recursive.py: -------------------------------------------------------------------------------- 1 | def rec(x): 2 | if x > 0: 3 | wat = x-1 4 | rec(wat) 5 | 6 | print('This is fun') 7 | rec(10) 8 | -------------------------------------------------------------------------------- /examples/example_inputs/simple.py: -------------------------------------------------------------------------------- 1 | x = 1 2 | -------------------------------------------------------------------------------- /examples/example_inputs/simple_function.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | print('h') 3 | 4 | 5 | y = input() 6 | foo() 7 | -------------------------------------------------------------------------------- /examples/example_inputs/simple_function_with_return.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | print('h') 3 | return 1 4 | 5 | def bar(): 6 | x = 2 7 | return x 8 | 9 | y = input() 10 | foo() 11 | x = bar() 12 | -------------------------------------------------------------------------------- /examples/example_inputs/single_comprehension.py: -------------------------------------------------------------------------------- 1 | ll = [(x, y) for x in [1,2,3] for y in [4,5,6]] 2 | -------------------------------------------------------------------------------- /examples/example_inputs/str_ignored.py: -------------------------------------------------------------------------------- 1 | x = 0 2 | ''' 3 | HEST 4 | ''' 5 | -------------------------------------------------------------------------------- /examples/example_inputs/ternary.py: -------------------------------------------------------------------------------- 1 | result = ( 2 | "abc" 3 | if t.u == v.w else 4 | "def" 5 | if x else 6 | y # This is the only RHS variable which taints result 7 | if func(z if 1 + 1 == 2 else z) else 8 | "ghi" 9 | ) 10 | -------------------------------------------------------------------------------- /examples/example_inputs/try.py: -------------------------------------------------------------------------------- 1 | try: 2 | value = None 3 | except ImportError: 4 | value = 1 5 | 6 | """ 7 | if node.orelse: 8 | orelse_last_nodes = self.handle_or_else(node.orelse, try_node) 9 | body.last_statements.extend(orelse_last_nodes) 10 | else: 11 | body.last_statements.append(try_node) # if there is no orelse, test needs an edge to the next_node 12 | if node.finalbody: 13 | finalbody_last_nodes = self.handle_or_else(node.finalbody, try_node) 14 | body.last_statements.extend(finalbody_last_nodes) 15 | else: 16 | body.last_statements.append(try_node) # if there is no orelse, test needs an edge to the next_node 17 | """ 18 | -------------------------------------------------------------------------------- /examples/example_inputs/try_final.py: -------------------------------------------------------------------------------- 1 | try: 2 | value = None 3 | except ImportError: 4 | value = 1 5 | finally: 6 | print('goodbye') 7 | -------------------------------------------------------------------------------- /examples/example_inputs/try_orelse.py: -------------------------------------------------------------------------------- 1 | def does_this_kill_us(diff): 2 | return subprocess.call(diff, shell=True) 3 | 4 | # @app.route('/poc', methods=['POST']) 5 | # def poc(): 6 | try: 7 | value = None 8 | print('A5') 9 | except ImportError: 10 | value = request.args.get('foo') 11 | print('Wagyu') 12 | else: 13 | does_this_kill_us(value) 14 | print('So') 15 | print('Good') 16 | -------------------------------------------------------------------------------- /examples/example_inputs/try_orelse_with_no_variables_to_save.py: -------------------------------------------------------------------------------- 1 | def does_this_kill_us(diff): 2 | return subprocess.call(diff, shell=True) 3 | 4 | # @app.route('/poc', methods=['POST']) 5 | # def poc(): 6 | try: 7 | print('A5') 8 | except ImportError: 9 | print('Wagyu') 10 | else: 11 | does_this_kill_us("hard-coded string") 12 | print('So') 13 | print('Good') 14 | -------------------------------------------------------------------------------- /examples/example_inputs/try_orelse_with_no_variables_to_save_and_no_args.py: -------------------------------------------------------------------------------- 1 | def does_this_kill_us(): 2 | return subprocess.call('ls', shell=True) 3 | 4 | # @app.route('/poc', methods=['POST']) 5 | # def poc(): 6 | try: 7 | print('A5') 8 | except ImportError: 9 | print('Wagyu') 10 | else: 11 | does_this_kill_us() 12 | print('So') 13 | print('Good') 14 | -------------------------------------------------------------------------------- /examples/example_inputs/while.py: -------------------------------------------------------------------------------- 1 | x = int(input()) 2 | while x < 10: 3 | x = x + 1 4 | if x == 5: 5 | break 6 | else: 7 | x = 6 8 | print(x) 9 | -------------------------------------------------------------------------------- /examples/example_inputs/while_break.py: -------------------------------------------------------------------------------- 1 | while x != 10: 2 | if x > 0: 3 | print(x) 4 | break 5 | else: 6 | print('hest') 7 | print('next') 8 | -------------------------------------------------------------------------------- /examples/example_inputs/while_complete.py: -------------------------------------------------------------------------------- 1 | while x > 0: 2 | x += 1 3 | x += 2 4 | else: 5 | x += 3 6 | x += 4 7 | x += 5 8 | -------------------------------------------------------------------------------- /examples/example_inputs/while_func_comparator.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | return True 3 | 4 | while foo(): 5 | print(x) 6 | x += 1 7 | -------------------------------------------------------------------------------- /examples/example_inputs/while_func_comparator_lhs.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | return 6 3 | 4 | while foo() > x: 5 | print(x) 6 | x += 1 7 | -------------------------------------------------------------------------------- /examples/example_inputs/while_func_comparator_rhs.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | return 6 3 | 4 | while x < foo(): 5 | print(x) 6 | x += 1 7 | -------------------------------------------------------------------------------- /examples/example_inputs/while_no_orelse.py: -------------------------------------------------------------------------------- 1 | while x > 0: 2 | x += 1 3 | x += 2 4 | x += 5 5 | -------------------------------------------------------------------------------- /examples/example_inputs/yield.py: -------------------------------------------------------------------------------- 1 | def func(x): 2 | yield x 3 | yield 2 4 | 5 | def main(): 6 | gen = func(1) 7 | print(next(gen)) 8 | x = 1 9 | y = 2 10 | print(next(gen)) 11 | -------------------------------------------------------------------------------- /examples/import_test/import.py: -------------------------------------------------------------------------------- 1 | import import_os 2 | import import_ast as a 3 | import import_math as _ 4 | -------------------------------------------------------------------------------- /examples/import_test/import_ast.py: -------------------------------------------------------------------------------- 1 | def bar(): 2 | pass 3 | -------------------------------------------------------------------------------- /examples/import_test/import_from.py: -------------------------------------------------------------------------------- 1 | from import_os import foo as f 2 | from import_ast import bar as b 3 | from import_math import num as n 4 | -------------------------------------------------------------------------------- /examples/import_test/import_math.py: -------------------------------------------------------------------------------- 1 | def num(): 2 | pass 3 | -------------------------------------------------------------------------------- /examples/import_test/import_os.py: -------------------------------------------------------------------------------- 1 | def foo(): 2 | pass 3 | -------------------------------------------------------------------------------- /examples/import_test_project/A.py: -------------------------------------------------------------------------------- 1 | def B(s): 2 | return s 3 | 4 | def C(s): 5 | return s + "see" 6 | 7 | def D(s): 8 | return s + "dee" 9 | -------------------------------------------------------------------------------- /examples/import_test_project/all_folder/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/examples/import_test_project/all_folder/__init__.py -------------------------------------------------------------------------------- /examples/import_test_project/all_folder/has_all.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | 'LemonGrass' 3 | ] 4 | 5 | def LemonGrass(): 6 | print ('LemonGrass') 7 | 8 | def MangoYuzu(): 9 | print ('MangoYuzu') 10 | -------------------------------------------------------------------------------- /examples/import_test_project/all_folder/no_all.py: -------------------------------------------------------------------------------- 1 | def _LemonGrass(): 2 | print ('LemonGrass') 3 | 4 | def MangoYuzu(): 5 | print ('MangoYuzu') 6 | -------------------------------------------------------------------------------- /examples/import_test_project/foo/bar.py: -------------------------------------------------------------------------------- 1 | def H(s): 2 | return s + "end" 3 | -------------------------------------------------------------------------------- /examples/import_test_project/multiple_files/A.py: -------------------------------------------------------------------------------- 1 | def cosme(s): 2 | return s + "aaa" 3 | -------------------------------------------------------------------------------- /examples/import_test_project/multiple_files/B.py: -------------------------------------------------------------------------------- 1 | def foo(s): 2 | return s + "bee" 3 | -------------------------------------------------------------------------------- /examples/import_test_project/multiple_files/C.py: -------------------------------------------------------------------------------- 1 | def foo(s): 2 | return s + "see" 3 | -------------------------------------------------------------------------------- /examples/import_test_project/multiple_files/D.py: -------------------------------------------------------------------------------- 1 | def foo(s): 2 | return s + "dee" 3 | -------------------------------------------------------------------------------- /examples/import_test_project/other_dir/test_from_dot_dot.py: -------------------------------------------------------------------------------- 1 | from .. import A 2 | 3 | 4 | c = A.B('sss') 5 | -------------------------------------------------------------------------------- /examples/import_test_project/other_dir/test_relative_between_folders.py: -------------------------------------------------------------------------------- 1 | from ..foo.bar import H 2 | 3 | result = H('hey') 4 | -------------------------------------------------------------------------------- /examples/import_test_project/package_star/A.py: -------------------------------------------------------------------------------- 1 | def cobia(): 2 | print ("A") 3 | -------------------------------------------------------------------------------- /examples/import_test_project/package_star/B.py: -------------------------------------------------------------------------------- 1 | def al(): 2 | print ("B") 3 | -------------------------------------------------------------------------------- /examples/import_test_project/package_star/__init__.py: -------------------------------------------------------------------------------- 1 | from . import A 2 | from . import B 3 | from . import folder 4 | -------------------------------------------------------------------------------- /examples/import_test_project/package_star/folder/C.py: -------------------------------------------------------------------------------- 1 | def pastor(): 2 | print ("C") 3 | -------------------------------------------------------------------------------- /examples/import_test_project/package_star/folder/__init__.py: -------------------------------------------------------------------------------- 1 | from . import C 2 | -------------------------------------------------------------------------------- /examples/import_test_project/package_star_with_alias/A.py: -------------------------------------------------------------------------------- 1 | def cobia(): 2 | print ("A") 3 | -------------------------------------------------------------------------------- /examples/import_test_project/package_star_with_alias/B.py: -------------------------------------------------------------------------------- 1 | def al(): 2 | print ("B") 3 | -------------------------------------------------------------------------------- /examples/import_test_project/package_star_with_alias/__init__.py: -------------------------------------------------------------------------------- 1 | from . import A as husk 2 | from . import B as meringue 3 | from . import folder as corn 4 | -------------------------------------------------------------------------------- /examples/import_test_project/package_star_with_alias/folder/C.py: -------------------------------------------------------------------------------- 1 | def pastor(): 2 | print ("C") 3 | -------------------------------------------------------------------------------- /examples/import_test_project/package_star_with_alias/folder/__init__.py: -------------------------------------------------------------------------------- 1 | from . import C as mousse 2 | -------------------------------------------------------------------------------- /examples/import_test_project/package_with_file/__init__.py: -------------------------------------------------------------------------------- 1 | from .nested_folder_without_init import Starbucks 2 | -------------------------------------------------------------------------------- /examples/import_test_project/package_with_file/nested_folder_without_init/Starbucks.py: -------------------------------------------------------------------------------- 1 | def Tea(): 2 | print ("Teavana Green") 3 | -------------------------------------------------------------------------------- /examples/import_test_project/package_with_file_and_alias/__init__.py: -------------------------------------------------------------------------------- 1 | from .nested_folder_without_init import Starbucks as Eataly 2 | -------------------------------------------------------------------------------- /examples/import_test_project/package_with_file_and_alias/nested_folder_without_init/Starbucks.py: -------------------------------------------------------------------------------- 1 | def Tea(): 2 | print ("Teavana Green") 3 | -------------------------------------------------------------------------------- /examples/import_test_project/package_with_folder/__init__.py: -------------------------------------------------------------------------------- 1 | from . import nested_folder_with_init 2 | -------------------------------------------------------------------------------- /examples/import_test_project/package_with_folder/nested_folder_with_init/__init__.py: -------------------------------------------------------------------------------- 1 | from . import moose 2 | -------------------------------------------------------------------------------- /examples/import_test_project/package_with_folder/nested_folder_with_init/moose.py: -------------------------------------------------------------------------------- 1 | def fast(): 2 | print ("real fast") 3 | -------------------------------------------------------------------------------- /examples/import_test_project/package_with_folder_and_alias/__init__.py: -------------------------------------------------------------------------------- 1 | from . import nested_folder_with_init as heyo 2 | -------------------------------------------------------------------------------- /examples/import_test_project/package_with_folder_and_alias/nested_folder_with_init/__init__.py: -------------------------------------------------------------------------------- 1 | from . import moose 2 | -------------------------------------------------------------------------------- /examples/import_test_project/package_with_folder_and_alias/nested_folder_with_init/moose.py: -------------------------------------------------------------------------------- 1 | def fast(): 2 | print ("real fast") 3 | -------------------------------------------------------------------------------- /examples/import_test_project/package_with_function/__init__.py: -------------------------------------------------------------------------------- 1 | from .nested_folder_with_init import StarbucksVisitor 2 | -------------------------------------------------------------------------------- /examples/import_test_project/package_with_function/nested_folder_with_init/__init__.py: -------------------------------------------------------------------------------- 1 | from .starbucks import StarbucksVisitor 2 | -------------------------------------------------------------------------------- /examples/import_test_project/package_with_function/nested_folder_with_init/starbucks.py: -------------------------------------------------------------------------------- 1 | def StarbucksVisitor(): 2 | print ("Iced Mocha") 3 | -------------------------------------------------------------------------------- /examples/import_test_project/package_with_function_and_alias/__init__.py: -------------------------------------------------------------------------------- 1 | from .nested_folder_with_init import StarbucksVisitor as EatalyVisitor 2 | -------------------------------------------------------------------------------- /examples/import_test_project/package_with_function_and_alias/nested_folder_with_init/__init__.py: -------------------------------------------------------------------------------- 1 | from .starbucks import StarbucksVisitor 2 | -------------------------------------------------------------------------------- /examples/import_test_project/package_with_function_and_alias/nested_folder_with_init/starbucks.py: -------------------------------------------------------------------------------- 1 | def StarbucksVisitor(): 2 | print ("Iced Mocha") 3 | -------------------------------------------------------------------------------- /examples/import_test_project/test_all.py: -------------------------------------------------------------------------------- 1 | from all_folder.has_all import * 2 | 3 | 4 | LemonGrass() 5 | # MangoYuzu is not defined because it is not in __all__ 6 | MangoYuzu() 7 | -------------------------------------------------------------------------------- /examples/import_test_project/test_from_directory.py: -------------------------------------------------------------------------------- 1 | from foo import bar 2 | 3 | bar.H('hey') 4 | -------------------------------------------------------------------------------- /examples/import_test_project/test_from_dot.py: -------------------------------------------------------------------------------- 1 | from . import A 2 | 3 | 4 | c = A.B('sss') 5 | -------------------------------------------------------------------------------- /examples/import_test_project/test_from_file_import_star.py: -------------------------------------------------------------------------------- 1 | from A import * 2 | 3 | 4 | B("60") 5 | C("minute") 6 | D("IPA") 7 | -------------------------------------------------------------------------------- /examples/import_test_project/test_from_package_import_star.py: -------------------------------------------------------------------------------- 1 | from package_star import * 2 | 3 | 4 | A.cobia() 5 | B.al() 6 | folder.C.pastor() 7 | -------------------------------------------------------------------------------- /examples/import_test_project/test_from_package_import_star_with_alias.py: -------------------------------------------------------------------------------- 1 | from package_star_with_alias import * 2 | 3 | 4 | husk.cobia() 5 | meringue.al() 6 | corn.mousse.pastor() 7 | -------------------------------------------------------------------------------- /examples/import_test_project/test_from_package_with_file.py: -------------------------------------------------------------------------------- 1 | from package_with_file import Starbucks 2 | 3 | 4 | Starbucks.Tea() 5 | -------------------------------------------------------------------------------- /examples/import_test_project/test_from_package_with_file_and_alias.py: -------------------------------------------------------------------------------- 1 | from package_with_file_and_alias import Eataly 2 | 3 | 4 | Eataly.Tea() 5 | -------------------------------------------------------------------------------- /examples/import_test_project/test_from_package_with_function.py: -------------------------------------------------------------------------------- 1 | from package_with_function import StarbucksVisitor 2 | 3 | 4 | StarbucksVisitor() 5 | -------------------------------------------------------------------------------- /examples/import_test_project/test_from_package_with_function_and_alias.py: -------------------------------------------------------------------------------- 1 | from package_with_function_and_alias import EatalyVisitor 2 | 3 | 4 | EatalyVisitor() 5 | -------------------------------------------------------------------------------- /examples/import_test_project/test_import.py: -------------------------------------------------------------------------------- 1 | from A import B 2 | import A 3 | 4 | 5 | b = B('str') 6 | c = A.B('sss') 7 | -------------------------------------------------------------------------------- /examples/import_test_project/test_import_as.py: -------------------------------------------------------------------------------- 1 | from A import B 2 | import A as foo 3 | b = B('str') 4 | c = foo.B('sss') 5 | -------------------------------------------------------------------------------- /examples/import_test_project/test_multiple_files_with_aliases.py: -------------------------------------------------------------------------------- 1 | from .multiple_files import A, B as keens, C as per_se, D as duck_house 2 | 3 | 4 | a = A.cosme('tlayuda') 5 | b = keens.foo('mutton') 6 | c = per_se.foo('tasting') 7 | d = duck_house.foo('peking') 8 | -------------------------------------------------------------------------------- /examples/import_test_project/test_multiple_functions_with_aliases.py: -------------------------------------------------------------------------------- 1 | from .A import B as keens, C, D as duck_house 2 | 3 | 4 | a = keens('mutton') 5 | b = C('tasting') 6 | c = duck_house('peking') 7 | -------------------------------------------------------------------------------- /examples/import_test_project/test_no_all.py: -------------------------------------------------------------------------------- 1 | from all_folder.no_all import * 2 | 3 | 4 | # _LemonGrass is not defined because it starts with _ 5 | _LemonGrass() 6 | MangoYuzu() 7 | -------------------------------------------------------------------------------- /examples/import_test_project/test_package_with_file.py: -------------------------------------------------------------------------------- 1 | import package_with_file 2 | 3 | 4 | package_with_file.Starbucks.Tea() 5 | -------------------------------------------------------------------------------- /examples/import_test_project/test_package_with_file_and_alias.py: -------------------------------------------------------------------------------- 1 | import package_with_file_and_alias 2 | 3 | 4 | package_with_file_and_alias.Eataly.Tea() 5 | -------------------------------------------------------------------------------- /examples/import_test_project/test_package_with_folder.py: -------------------------------------------------------------------------------- 1 | import package_with_folder 2 | 3 | package_with_folder.nested_folder_with_init.moose.fast() 4 | -------------------------------------------------------------------------------- /examples/import_test_project/test_package_with_folder_and_alias.py: -------------------------------------------------------------------------------- 1 | import package_with_folder_and_alias 2 | 3 | package_with_folder_and_alias.heyo.moose.fast() 4 | -------------------------------------------------------------------------------- /examples/import_test_project/test_package_with_function.py: -------------------------------------------------------------------------------- 1 | import package_with_function 2 | 3 | 4 | package_with_function.StarbucksVisitor() 5 | -------------------------------------------------------------------------------- /examples/import_test_project/test_package_with_function_and_alias.py: -------------------------------------------------------------------------------- 1 | import package_with_function_and_alias 2 | 3 | 4 | package_with_function_and_alias.EatalyVisitor() 5 | -------------------------------------------------------------------------------- /examples/import_test_project/test_relative_from_directory.py: -------------------------------------------------------------------------------- 1 | # Must be run as module via -m 2 | from .foo import bar 3 | 4 | 5 | bar.H('hey') 6 | -------------------------------------------------------------------------------- /examples/import_test_project/test_relative_level_1.py: -------------------------------------------------------------------------------- 1 | from .A import B 2 | import A 3 | b = B('str') 4 | c = A.B('sss') 5 | -------------------------------------------------------------------------------- /examples/import_test_project/test_relative_level_2.py: -------------------------------------------------------------------------------- 1 | from ..A import B 2 | import A 3 | b = B('str') 4 | c = A.B('sss') 5 | -------------------------------------------------------------------------------- /examples/nested_functions_code/builtin_with_user_defined_inner.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from flask import Flask, render_template, request 3 | 4 | # This is a lib we can't possibly see inside of 5 | import scrypt 6 | 7 | 8 | app = Flask(__name__) 9 | 10 | def inner(inner_arg): 11 | yes_vuln = inner_arg + 'hey' 12 | return yes_vuln 13 | 14 | @app.route('/menu', methods=['POST']) 15 | def menu(): 16 | req_param = request.form['suggestion'] 17 | 18 | # blackbox(user_defined_inner()) 19 | foo = scrypt.encrypt(inner(req_param)) 20 | subprocess.call(foo, shell=True) 21 | 22 | with open('menu.txt','r') as f: 23 | menu = f.read() 24 | 25 | return render_template('command_injection.html', menu=menu) 26 | -------------------------------------------------------------------------------- /examples/nested_functions_code/nested_user_defined_function_calls.py: -------------------------------------------------------------------------------- 1 | def outer(outer_arg): 2 | outer_ret_val = outer_arg + 'hey' 3 | return outer_ret_val 4 | 5 | def inner(inner_arg): 6 | inner_ret_val = inner_arg + 'hey' 7 | return inner_ret_val 8 | 9 | foo = 'bar' 10 | abc = outer(inner(foo)) 11 | -------------------------------------------------------------------------------- /examples/nested_functions_code/sink_with_blackbox_inner.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from flask import Flask, render_template, request 3 | 4 | # This is a lib we can't possibly see inside of 5 | import scrypt 6 | 7 | 8 | app = Flask(__name__) 9 | 10 | @app.route('/menu', methods=['POST']) 11 | def menu(): 12 | req_param = request.form['suggestion'] 13 | 14 | subprocess.call(scrypt.encypt(scrypt.encypt(req_param)), shell=True) 15 | 16 | with open('menu.txt','r') as f: 17 | menu = f.read() 18 | 19 | return render_template('command_injection.html', menu=menu) 20 | -------------------------------------------------------------------------------- /examples/nested_functions_code/sink_with_result_of_blackbox_nested.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from flask import Flask, render_template, request 3 | 4 | # This is a lib we can't possibly see inside of 5 | import scrypt 6 | 7 | 8 | app = Flask(__name__) 9 | 10 | @app.route('/menu', methods=['POST']) 11 | def menu(): 12 | req_param = request.form['suggestion'] 13 | result = scrypt.encrypt(scrypt.encrypt(req_param)) 14 | subprocess.call(result, shell=True) 15 | 16 | with open('menu.txt','r') as f: 17 | menu = f.read() 18 | 19 | return render_template('command_injection.html', menu=menu) 20 | -------------------------------------------------------------------------------- /examples/nested_functions_code/sink_with_result_of_user_defined_nested.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from flask import Flask, render_template, request 3 | 4 | app = Flask(__name__) 5 | 6 | def outer(outer_arg): 7 | outer_ret_val = outer_arg + 'hey' 8 | return outer_ret_val 9 | 10 | def inner(inner_arg): 11 | inner_ret_val = inner_arg + 'hey' 12 | return inner_ret_val 13 | 14 | @app.route('/menu', methods=['POST']) 15 | def menu(): 16 | req_param = request.form['suggestion'] 17 | result = outer(inner(req_param)) 18 | subprocess.call(result, shell=True) 19 | 20 | with open('menu.txt','r') as f: 21 | menu = f.read() 22 | 23 | return render_template('command_injection.html', menu=menu) 24 | -------------------------------------------------------------------------------- /examples/nested_functions_code/sink_with_user_defined_inner.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from flask import Flask, render_template, request 3 | 4 | app = Flask(__name__) 5 | 6 | def outer(outer_arg): 7 | outer_ret_val = outer_arg + 'hey' 8 | return outer_ret_val 9 | 10 | def inner(inner_arg): 11 | inner_ret_val = inner_arg + 'hey' 12 | return inner_ret_val 13 | 14 | @app.route('/menu', methods=['POST']) 15 | def menu(): 16 | req_param = request.form['suggestion'] 17 | 18 | subprocess.call(outer(inner(req_param)), shell=True) 19 | 20 | with open('menu.txt','r') as f: 21 | menu = f.read() 22 | 23 | return render_template('command_injection.html', menu=menu) 24 | -------------------------------------------------------------------------------- /examples/report_cfg_examples/ast_example.py: -------------------------------------------------------------------------------- 1 | x = 1 2 | -------------------------------------------------------------------------------- /examples/report_cfg_examples/decorator.py: -------------------------------------------------------------------------------- 1 | class LabelDecorator(object): 2 | def __init__(self, number): 3 | self.number = number 4 | 5 | def __call__(self, f): 6 | print('Function: ', self.number) 7 | 8 | return f 9 | 10 | 11 | @LabelDecorator(1) 12 | def foo(a, b): 13 | return a + b 14 | 15 | print(foo(3, 4)) 16 | -------------------------------------------------------------------------------- /examples/report_cfg_examples/for_else.py: -------------------------------------------------------------------------------- 1 | for x in range(5): 2 | if invalid_value(x): 3 | break 4 | print(x) 5 | else: 6 | print('Accepted') 7 | -------------------------------------------------------------------------------- /examples/report_cfg_examples/if.py: -------------------------------------------------------------------------------- 1 | if True: 2 | x = 0 3 | -------------------------------------------------------------------------------- /examples/report_cfg_examples/if_else.py: -------------------------------------------------------------------------------- 1 | if True: 2 | x = 0 3 | else: 4 | y = 0 5 | -------------------------------------------------------------------------------- /examples/report_cfg_examples/if_else_elif.py: -------------------------------------------------------------------------------- 1 | if True: 2 | x = 0 3 | elif False: 4 | y = 0 5 | else: 6 | z = 0 7 | -------------------------------------------------------------------------------- /examples/report_cfg_examples/sequence.py: -------------------------------------------------------------------------------- 1 | x = 3 2 | y = 5 3 | -------------------------------------------------------------------------------- /examples/report_cfg_examples/simple.py: -------------------------------------------------------------------------------- 1 | x = 3 2 | -------------------------------------------------------------------------------- /examples/report_cfg_examples/small_program.py: -------------------------------------------------------------------------------- 1 | x = 3 2 | y = 0 3 | while x > 0: 4 | print(x) 5 | x -= 1 6 | y += x 7 | 8 | if y == 5: 9 | print('five') 10 | else: 11 | print('close to five') 12 | -------------------------------------------------------------------------------- /examples/report_cfg_examples/while.py: -------------------------------------------------------------------------------- 1 | while True: 2 | x += 1 3 | -------------------------------------------------------------------------------- /examples/report_cfg_examples/while_break.py: -------------------------------------------------------------------------------- 1 | while x < threshold: 2 | if invalid_value(x): 3 | break 4 | x += 1 5 | else: 6 | handle_value() 7 | -------------------------------------------------------------------------------- /examples/report_cfg_examples/while_else.py: -------------------------------------------------------------------------------- 1 | x = 0 2 | while x < 5: 3 | print(x) 4 | x += 1 5 | else: 6 | print('h') 7 | -------------------------------------------------------------------------------- /examples/report_cfg_examples/while_else_2.py: -------------------------------------------------------------------------------- 1 | while x < 100: 2 | x += 1 3 | x += 2 4 | else: 5 | x += 3 6 | x += 4 7 | -------------------------------------------------------------------------------- /examples/test_project/app.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/examples/test_project/app.py -------------------------------------------------------------------------------- /examples/test_project/exceptions.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/examples/test_project/exceptions.py -------------------------------------------------------------------------------- /examples/test_project/folder/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/examples/test_project/folder/__init__.py -------------------------------------------------------------------------------- /examples/test_project/folder/directory/indhold.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/examples/test_project/folder/directory/indhold.py -------------------------------------------------------------------------------- /examples/test_project/folder/file.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/examples/test_project/folder/file.txt -------------------------------------------------------------------------------- /examples/test_project/folder/some.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/examples/test_project/folder/some.py -------------------------------------------------------------------------------- /examples/test_project/license.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/examples/test_project/license.txt -------------------------------------------------------------------------------- /examples/test_project/utils.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/examples/test_project/utils.py -------------------------------------------------------------------------------- /examples/vulnerable_code/XSS.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, make_response 2 | app = Flask(__name__) 3 | 4 | @app.route('/XSS_param', methods =['GET']) 5 | def XSS1(): 6 | param = request.args.get('param', 'not set') 7 | 8 | html = open('templates/xss.html').read() 9 | resp = make_response(html.replace('{{ param }}', param)) 10 | return resp 11 | 12 | if __name__ == '__main__': 13 | app.run(debug= True) 14 | -------------------------------------------------------------------------------- /examples/vulnerable_code/XSS_assign_to_other_var.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, make_response 2 | app = Flask(__name__) 3 | 4 | @app.route('/XSS_param', methods =['GET']) 5 | def XSS1(): 6 | param = request.args.get('param', 'not set') 7 | 8 | other_var = param 9 | 10 | html = open('templates/XSS_param.html').read() 11 | resp = make_response(html.replace('{{ param }}', other_var)) 12 | return resp 13 | 14 | if __name__ == '__main__': 15 | app.run(debug= True) 16 | -------------------------------------------------------------------------------- /examples/vulnerable_code/XSS_call.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, make_response 2 | app = Flask(__name__) 3 | 4 | class AwesomeString(): 5 | 6 | def __init__(self, s): 7 | self.s = s 8 | 9 | def dum(p): 10 | p = '' 11 | 12 | @app.route('/XSS_param', methods =['GET']) 13 | def XSS1(): 14 | param = request.args.get('param', 'not set') 15 | pik = dum(param) 16 | 17 | html = open('templates/XSS_param.html').read() 18 | resp = make_response(html.replace('{{ param }}', pik)) 19 | return resp 20 | 21 | if __name__ == '__main__': 22 | app.run(debug= True) 23 | -------------------------------------------------------------------------------- /examples/vulnerable_code/XSS_form.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request, make_response 2 | import random 3 | import string 4 | app = Flask(__name__) 5 | 6 | @app.route('/example2') 7 | def example2(): 8 | return render_template('example2_form.html') 9 | 10 | html1 = open('templates/example2_response.html').read() 11 | 12 | @app.route('/example2action',methods = ['POST']) 13 | def example2_action(): 14 | data = request.form['my_text'] 15 | resp = make_response(html1.replace('{{ data }}', data )) 16 | resp.set_cookie('session_id', ''.join(random.choice(string.ascii_uppercase) for x in range(16))) 17 | return resp 18 | 19 | 20 | if __name__ == '__main__': 21 | app.run(debug= True) 22 | -------------------------------------------------------------------------------- /examples/vulnerable_code/XSS_no_vuln.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, make_response 2 | app = Flask(__name__) 3 | 4 | @app.route('/XSS_param', methods =['GET']) 5 | def XSS1(): 6 | param = request.args.get('param', 'not set') 7 | 8 | html = open('templates/XSS_param.html').read() 9 | other = '' 10 | resp = make_response(html.replace('{{ param }}', other)) 11 | return resp 12 | 13 | if __name__ == '__main__': 14 | app.run(debug= True) 15 | -------------------------------------------------------------------------------- /examples/vulnerable_code/XSS_reassign.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, make_response 2 | app = Flask(__name__) 3 | 4 | @app.route('/XSS_param', methods =['GET']) 5 | def XSS1(): 6 | param = request.args.get('param', 'not set') 7 | 8 | param = param + '' 9 | 10 | html = open('templates/XSS_param.html').read() 11 | resp = make_response(html.replace('{{ param }}', param)) 12 | return resp 13 | 14 | if __name__ == '__main__': 15 | app.run(debug= True) 16 | -------------------------------------------------------------------------------- /examples/vulnerable_code/XSS_sanitised.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, make_response, Markup 2 | 3 | app = Flask(__name__) 4 | 5 | @app.route('/XSS_param', methods =['GET']) 6 | def XSS1(): 7 | param = request.args.get('param', 'not set') 8 | 9 | param = Markup.escape(param) 10 | 11 | html = open('templates/XSS_param.html').read() 12 | resp = make_response(html.replace('{{ param }}', param)) 13 | return resp 14 | 15 | if __name__ == '__main__': 16 | app.run(debug= True) 17 | -------------------------------------------------------------------------------- /examples/vulnerable_code/XSS_url.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, make_response 2 | app = Flask(__name__) 3 | 4 | @app.route('/XSS_param/', methods =['GET']) 5 | def XSS1(url): 6 | param = url 7 | 8 | html = open('templates/XSS_param.html').read() 9 | resp = make_response(html.replace('{{ param }}', param)) 10 | return resp 11 | 12 | if __name__ == '__main__': 13 | app.run(debug= True) 14 | -------------------------------------------------------------------------------- /examples/vulnerable_code/XSS_variable_assign.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, make_response 2 | app = Flask(__name__) 3 | 4 | @app.route('/XSS_param', methods =['GET']) 5 | def XSS1(): 6 | param = request.args.get('param', 'not set') 7 | 8 | other_var = param + '' 9 | 10 | html = open('templates/XSS_param.html').read() 11 | resp = make_response(html.replace('{{ param }}', other_var)) 12 | return resp 13 | 14 | if __name__ == '__main__': 15 | app.run(debug= True) 16 | -------------------------------------------------------------------------------- /examples/vulnerable_code/XSS_variable_assign_no_vuln.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, make_response 2 | app = Flask(__name__) 3 | 4 | @app.route('/XSS_param', methods =['GET']) 5 | def XSS1(): 6 | param = request.args.get('param', 'not set') 7 | 8 | other_var = param + '' 9 | 10 | html = open('templates/XSS_param.html').read() 11 | not_dangerous = "" 12 | resp = make_response(html.replace('{{ param }}', not_dangerous)) 13 | return resp 14 | 15 | if __name__ == '__main__': 16 | app.run(debug= True) 17 | -------------------------------------------------------------------------------- /examples/vulnerable_code/XSS_variable_multiple_assign.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request, make_response 2 | app = Flask(__name__) 3 | 4 | @app.route('/XSS_param', methods =['GET']) 5 | def XSS1(): 6 | param = request.args.get('param', 'not set') 7 | 8 | other_var = param + '' 9 | 10 | not_the_same_var = '' + other_var 11 | 12 | another_one = not_the_same_var + '' 13 | 14 | html = open('templates/XSS_param.html').read() 15 | resp = make_response(html.replace('{{ param }}', another_one)) 16 | 17 | return resp 18 | 19 | if __name__ == '__main__': 20 | app.run(debug= True) 21 | -------------------------------------------------------------------------------- /examples/vulnerable_code/blackbox_call_after_if.py: -------------------------------------------------------------------------------- 1 | import scrypt 2 | 3 | 4 | image_name = request.args.get('image_name') 5 | if not image_name: 6 | image_name = 'foo' 7 | foo = scrypt.outer(image_name) # Any call after ControlFlowNode caused the problem 8 | send_file(foo) 9 | -------------------------------------------------------------------------------- /examples/vulnerable_code/command_injection.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from flask import Flask, render_template, request 3 | 4 | app = Flask(__name__) 5 | 6 | @app.route('/') 7 | def index(): 8 | with open('menu.txt','r') as f: 9 | menu = f.read() 10 | 11 | return render_template('command_injection.html', menu=menu) 12 | 13 | @app.route('/menu', methods=['POST']) 14 | def menu(): 15 | param = request.form['suggestion'] 16 | command = 'echo ' + param + ' >> ' + 'menu.txt' 17 | 18 | subprocess.call(command, shell=True) 19 | 20 | with open('menu.txt','r') as f: 21 | menu = f.read() 22 | 23 | return render_template('command_injection.html', menu=menu) 24 | 25 | @app.route('/clean') 26 | def clean(): 27 | subprocess.call('echo Menu: > menu.txt', shell=True) 28 | 29 | with open('menu.txt','r') as f: 30 | menu = f.read() 31 | 32 | return render_template('command_injection.html', menu=menu) 33 | 34 | if __name__ == '__main__': 35 | app.run(debug=True) 36 | -------------------------------------------------------------------------------- /examples/vulnerable_code/command_injection_with_aliases.py: -------------------------------------------------------------------------------- 1 | import os 2 | import os as myos 3 | from os import system 4 | from os import system as mysystem 5 | from subprocess import call as mycall, Popen as mypopen 6 | 7 | from flask import Flask, render_template, request 8 | 9 | app = Flask(__name__) 10 | 11 | 12 | @app.route('/menu', methods=['POST']) 13 | def menu(): 14 | param = request.form['suggestion'] 15 | command = 'echo ' + param + ' >> ' + 'menu.txt' 16 | 17 | os.system(command) 18 | myos.system(command) 19 | system(command) 20 | mysystem(command) 21 | mycall(command) 22 | mypopen(command) 23 | 24 | with open('menu.txt', 'r') as f: 25 | menu_ctx = f.read() 26 | 27 | return render_template('command_injection.html', menu=menu_ctx) 28 | 29 | 30 | if __name__ == '__main__': 31 | app.run(debug=True) 32 | -------------------------------------------------------------------------------- /examples/vulnerable_code/django_XSS.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | 4 | def xss1(request, param): 5 | return render(request, 'templates/xss.html', {'param': param}) 6 | -------------------------------------------------------------------------------- /examples/vulnerable_code/ensure_saved_scope.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, request, send_file 3 | 4 | app = Flask(__name__) 5 | 6 | def outer(outer_arg, other_arg): 7 | outer_ret_val = outer_arg + 'hey' + other_arg 8 | return outer_ret_val 9 | 10 | def inner(): 11 | return 'boom' 12 | 13 | @app.route('/') 14 | def cat_picture(): 15 | image_name = request.args.get('image_name') 16 | if not image_name: 17 | image_name = 'foo' 18 | return 404 19 | foo = outer(inner(), image_name) # Nested call after if caused the problem 20 | send_file(image_name) 21 | return 'idk' 22 | 23 | 24 | if __name__ == '__main__': 25 | app.run(debug=True) 26 | -------------------------------------------------------------------------------- /examples/vulnerable_code/inter_command_injection.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from flask import Flask, render_template, request 3 | 4 | 5 | app = Flask(__name__) 6 | 7 | @app.route('/') 8 | def index(): 9 | with open('menu.txt','r') as f: 10 | menu = f.read() 11 | 12 | return render_template('command_injection.html', menu=menu) 13 | 14 | def shell_the_arg(arg): 15 | subprocess.call(arg, shell=True) 16 | 17 | @app.route('/menu', methods=['POST']) 18 | def menu(): 19 | param = request.form['suggestion'] 20 | 21 | shell_the_arg('echo ' + param + ' >> ' + 'menu.txt') 22 | 23 | with open('menu.txt','r') as f: 24 | menu = f.read() 25 | 26 | return render_template('command_injection.html', menu=menu) 27 | 28 | @app.route('/clean') 29 | def clean(): 30 | subprocess.call('echo Menu: > menu.txt', shell=True) 31 | 32 | with open('menu.txt','r') as f: 33 | menu = f.read() 34 | 35 | return render_template('command_injection.html', menu=menu) 36 | 37 | if __name__ == '__main__': 38 | app.run(debug=True) 39 | -------------------------------------------------------------------------------- /examples/vulnerable_code/inter_command_injection_2.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from flask import Flask, render_template, request 3 | 4 | 5 | app = Flask(__name__) 6 | 7 | @app.route('/') 8 | def index(): 9 | with open('menu.txt','r') as f: 10 | menu = f.read() 11 | 12 | return render_template('command_injection.html', menu=menu) 13 | 14 | def return_the_arg(foo): 15 | return foo 16 | 17 | @app.route('/menu', methods=['POST']) 18 | def menu(): 19 | param = request.form['suggestion'] 20 | 21 | command = return_the_arg('echo ' + param + ' >> ' + 'menu.txt') 22 | subprocess.call(command, shell=True) 23 | 24 | with open('menu.txt','r') as f: 25 | menu = f.read() 26 | 27 | return render_template('command_injection.html', menu=menu) 28 | 29 | @app.route('/clean') 30 | def clean(): 31 | subprocess.call('echo Menu: > menu.txt', shell=True) 32 | 33 | with open('menu.txt','r') as f: 34 | menu = f.read() 35 | 36 | return render_template('command_injection.html', menu=menu) 37 | 38 | if __name__ == '__main__': 39 | app.run(debug=True) 40 | -------------------------------------------------------------------------------- /examples/vulnerable_code/list_append.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import request 4 | 5 | 6 | def func(): 7 | TAINT = request.args.get("TAINT") 8 | 9 | cmd = [] 10 | cmd.append("echo") 11 | cmd.append(TAINT) 12 | 13 | os.system(" ".join(cmd)) 14 | -------------------------------------------------------------------------------- /examples/vulnerable_code/menu.txt: -------------------------------------------------------------------------------- 1 | Menu: 2 | -------------------------------------------------------------------------------- /examples/vulnerable_code/multi_chain.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from flask import Flask, render_template, request 3 | 4 | 5 | app = Flask(__name__) 6 | 7 | 8 | @app.route('/multi_chain', methods=['POST']) 9 | def multi_chain(): 10 | suggestion = request.form['suggestion'] 11 | x = fast_eddie(suggestion, 'the') 12 | y = x + 'foo' 13 | z = minnesota_fats(suggestion, 'sting') 14 | ben = graham(y, z) 15 | 16 | subprocess.call(ben, shell=True) 17 | 18 | return render_template('multi_chain.html') 19 | -------------------------------------------------------------------------------- /examples/vulnerable_code/multiple_blackbox_calls_in_user_defined_call_after_if.py: -------------------------------------------------------------------------------- 1 | import scrypt 2 | 3 | 4 | def outer(first_arg, second_arg, third_arg): 5 | outer_ret_val = first_arg + second_arg + third_arg 6 | return outer_ret_val 7 | 8 | def second_inner(inner_arg): 9 | inner_ret_val = inner_arg + '2nd' 10 | return inner_ret_val 11 | 12 | image_name = request.args.get('image_name') 13 | if not image_name: 14 | image_name = 'foo' 15 | foo = outer(scrypt.first_inner(image_name), second_inner(image_name), scrypt.third_inner(image_name)) # Any call after ControlFlowNode caused the problem 16 | send_file(foo) 17 | -------------------------------------------------------------------------------- /examples/vulnerable_code/multiple_nested_blackbox_calls_after_for.py: -------------------------------------------------------------------------------- 1 | import scrypt 2 | 3 | 4 | image_name = request.args.get('image_name') 5 | for x in range(0, 10): 6 | print(x) 7 | foo = scrypt.outer(scrypt.inner(image_name), scrypt.other_inner(image_name)) # Any call after ControlFlowNode caused the problem 8 | send_file(foo) 9 | -------------------------------------------------------------------------------- /examples/vulnerable_code/multiple_nested_user_defined_calls_after_if.py: -------------------------------------------------------------------------------- 1 | def outer(first_arg, second_arg): 2 | outer_ret_val = first_arg + second_arg 3 | return outer_ret_val 4 | 5 | def first_inner(first_inner_arg): 6 | first_inner_ret_val = first_inner_arg + '1st' 7 | return first_inner_ret_val 8 | 9 | def second_inner(second_inner_arg): 10 | second_inner_ret_val = second_inner_arg + '2nd' 11 | return second_inner_ret_val 12 | # return 'foo' 13 | 14 | image_name = request.args.get('image_name') 15 | if not image_name: 16 | image_name = 'foo' 17 | foo = outer(first_inner(image_name), second_inner(image_name)) # Any call after ControlFlowNode caused the problem 18 | send_file(foo) 19 | -------------------------------------------------------------------------------- /examples/vulnerable_code/multiple_user_defined_calls_in_blackbox_call_after_if.py: -------------------------------------------------------------------------------- 1 | import scrypt 2 | 3 | 4 | def first_inner(first_arg): 5 | first_ret_val = first_arg + '1st' 6 | return first_ret_val 7 | 8 | def third_inner(second_arg): 9 | third_ret_val = second_arg + '2nd' 10 | return third_ret_val 11 | 12 | image_name = request.args.get('image_name') 13 | if not image_name: 14 | image_name = 'foo' 15 | foo = scrypt.outer(first_inner(image_name), scrypt.second_inner(image_name), third_inner(image_name)) # Any call after ControlFlowNode caused the problem 16 | send_file(foo) 17 | -------------------------------------------------------------------------------- /examples/vulnerable_code/path_traversal.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, request, send_file 3 | 4 | app = Flask(__name__) 5 | 6 | def outer(outer_arg, other_arg): 7 | outer_ret_val = outer_arg + 'hey' + other_arg 8 | return outer_ret_val 9 | 10 | def inner(): 11 | return 'boom' 12 | 13 | @app.route('/') 14 | def cat_picture(): 15 | image_name = request.args.get('image_name') 16 | if not image_name: 17 | image_name = 'foo' 18 | return 404 19 | foo = outer(inner(), image_name) # Nested call after if caused the problem 20 | send_file(foo) 21 | return 'idk' 22 | 23 | 24 | if __name__ == '__main__': 25 | app.run(debug=True) 26 | -------------------------------------------------------------------------------- /examples/vulnerable_code/path_traversal_sanitised.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, request, send_file 3 | 4 | app = Flask(__name__) 5 | 6 | @app.route('/') 7 | def cat_picture(): 8 | image_name = request.args.get('image_name') 9 | 10 | image_name = image_name.replace('..', '') 11 | 12 | return send_file(os.path.join(os.getcwd(), image_name)) 13 | 14 | if __name__ == '__main__': 15 | app.run(debug=True) 16 | -------------------------------------------------------------------------------- /examples/vulnerable_code/path_traversal_sanitised_2.py: -------------------------------------------------------------------------------- 1 | import os 2 | from flask import Flask, request, send_file 3 | 4 | app = Flask(__name__) 5 | 6 | @app.route('/') 7 | def cat_picture(): 8 | image_name = request.args.get('image_name') 9 | 10 | if '..' in image_name: 11 | return 404 12 | return send_file(os.path.join(os.getcwd(), image_name)) 13 | 14 | if __name__ == '__main__': 15 | app.run(debug=True) 16 | -------------------------------------------------------------------------------- /examples/vulnerable_code/recursive.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | 3 | app = Flask(__name__) 4 | 5 | 6 | def recur_without_any_propagation(x): 7 | if len(x) < 20: 8 | return recur_without_any_propagation("a" * 24) 9 | return "Done" 10 | 11 | 12 | def recur_no_propagation_false_positive(x): 13 | if len(x) < 20: 14 | return recur_no_propagation_false_positive(x + "!") 15 | return "Done" 16 | 17 | 18 | def recur_with_propagation(x): 19 | if len(x) < 20: 20 | return recur_with_propagation(x + "!") 21 | return x 22 | 23 | 24 | @app.route('/recursive') 25 | def route(): 26 | param = request.args.get('param', 'not set') 27 | repeated_completely_untainted = recur_without_any_propagation(param) 28 | app.db.execute(repeated_completely_untainted) 29 | repeated_untainted = recur_no_propagation_false_positive(param) 30 | app.db.execute(repeated_untainted) 31 | repeated_tainted = recur_with_propagation(param) 32 | app.db.execute(repeated_tainted) 33 | -------------------------------------------------------------------------------- /examples/vulnerable_code/render_ids.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template 2 | 3 | 4 | app = Flask(__name__) 5 | 6 | 7 | @app.route('/') 8 | def index(): 9 | # Expected value 10 | ids = [u"one", u"two's", u'"three"'] 11 | # Injected somehow 12 | ids = ' onmouseover=alert(1) ' 13 | 14 | return render_template('render_ids.html', ids=ids) 15 | 16 | if __name__ == '__main__': 17 | app.run(port=80, debug=True) 18 | -------------------------------------------------------------------------------- /examples/vulnerable_code/simple_vulnerability.py: -------------------------------------------------------------------------------- 1 | print('this application is vulnerable') 2 | x = input() 3 | print('the user can execute random python code') 4 | eval(x) 5 | print('this is dangerous!') 6 | -------------------------------------------------------------------------------- /examples/vulnerable_code/sql/init_db.py: -------------------------------------------------------------------------------- 1 | from sqli import db 2 | from sqli import User 3 | 4 | 5 | db.create_all() 6 | 7 | def add_user(name, email): 8 | u = User(name, email) 9 | db.session.add(u) 10 | 11 | 12 | add_user('hest', 'hest') 13 | add_user('føl', 'føl') 14 | 15 | db.session.commit() 16 | -------------------------------------------------------------------------------- /examples/vulnerable_code/sql/sqli.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | from flask_sqlalchemy import SQLAlchemy 3 | from sqlalchemy.orm import sessionmaker 4 | import sys 5 | 6 | app = Flask(__name__) 7 | 8 | # SQL Alchemy setup 9 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////tmp/test.db' 10 | db = SQLAlchemy(app) 11 | 12 | class User(db.Model): 13 | id = db.Column(db.Integer, primary_key=True) 14 | username = db.Column(db.String(80), unique=True) 15 | email = db.Column(db.String(120), unique=True) 16 | 17 | def __init__(self, username, email): 18 | self.username = username 19 | self.email = email 20 | 21 | def __repr__(self): 22 | return '' % self.username 23 | 24 | @app.route('/raw') 25 | def index(): 26 | param = request.args.get('param', 'not set') 27 | result = db.engine.execute(param) 28 | print(User.query.all(), file=sys.stderr) 29 | return 'Result is displayed in console.' 30 | 31 | @app.route('/filtering') 32 | def filtering(): 33 | param = request.args.get('param', 'not set') 34 | Session = sessionmaker(bind=db.engine) 35 | session = Session() 36 | result = session.query(User).filter("username={}".format(param)) 37 | for value in result: 38 | print(value.username, value.email) 39 | return 'Result is displayed in console.' 40 | 41 | @app.route('/users/', methods=['DELETE']) 42 | def delete_user_dangerously(name): 43 | query = "DELETE FROM user WHERE username = :name" 44 | db.engine.execute(query, name=name) 45 | print('Deleted') 46 | return 'Deleted' 47 | 48 | 49 | if __name__ == '__main__': 50 | app.run(debug=True) 51 | -------------------------------------------------------------------------------- /examples/vulnerable_code/tainted_arg_normal_function.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | def store_uploaded_file(title, uploaded_file): 5 | """ Stores a temporary uploaded file on disk """ 6 | upload_dir_path = '%s/static/taskManager/uploads' % ( 7 | os.path.dirname(os.path.realpath(__file__))) 8 | if not os.path.exists(upload_dir_path): 9 | os.makedirs(upload_dir_path) 10 | 11 | # A1: Injection (shell) 12 | os.system( 13 | "mv " + 14 | uploaded_file.temporary_file_path() + 15 | " " + 16 | "%s/%s" % 17 | (upload_dir_path, 18 | title)) 19 | 20 | return '/static/taskManager/uploads/%s' % (title) 21 | -------------------------------------------------------------------------------- /examples/vulnerable_code/templates/XSS_param.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello from Flask 4 | Hello {{ param }}! 5 | 6 | -------------------------------------------------------------------------------- /examples/vulnerable_code/templates/attackerSite.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | hest 5 | 6 |
7 | var u = 'http://'> 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/vulnerable_code/templates/command_injection.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 | 6 |
7 | 8 |
9 |
10 | {{menu}} 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/vulnerable_code/templates/example2_form.html: -------------------------------------------------------------------------------- 1 | 2 | hest 3 | 4 |
5 | 6 | 7 |
8 | 9 | -------------------------------------------------------------------------------- /examples/vulnerable_code/templates/example2_response.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Hello from Flask 4 | Hello {{ data }}! 5 | 6 | -------------------------------------------------------------------------------- /examples/vulnerable_code/templates/link.html: -------------------------------------------------------------------------------- 1 | Link! 2 | -------------------------------------------------------------------------------- /examples/vulnerable_code/templates/render_ids.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Testing 4 | 20 | 21 | 22 |

Testing

23 | 24 |
25 |

26 | Hover over this box to see the unintended consequence of 27 | using a seemingly safe mix of "|tojson" + standard double-quoted HTML attributes. 28 |

29 |
30 | 31 |
32 |

33 | Note that in a real application, "data-ids" is a clean and 34 | unobtrusive way to pass data to an external script, all 35 | without having any "wire-up" code in your HTML. 36 |

37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /examples/vulnerable_code/yield.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from flask import Flask, request 3 | 4 | app = Flask(__name__) 5 | 6 | 7 | def things_to_run(): 8 | yield "echo hello" 9 | yield from request.get_json()["commands"] 10 | yield "echo done" 11 | 12 | 13 | @app.route('/', methods=['POST']) 14 | def home(): 15 | script = "; ".join(things_to_run()) 16 | subprocess.call(script, shell=True) 17 | return 'Executed' 18 | 19 | 20 | if __name__ == '__main__': 21 | app.run(debug=True) 22 | -------------------------------------------------------------------------------- /examples/vulnerable_code_across_files/absolute_from_file_command_injection.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request 2 | 3 | from other_file import shell_the_arg 4 | 5 | 6 | app = Flask(__name__) 7 | 8 | @app.route('/menu', methods=['POST']) 9 | def menu(): 10 | param = request.form['suggestion'] 11 | 12 | shell_the_arg('echo ' + param + ' >> ' + 'menu.txt') 13 | 14 | with open('menu.txt','r') as f: 15 | menu = f.read() 16 | 17 | return render_template('command_injection.html', menu=menu) 18 | 19 | if __name__ == '__main__': 20 | app.run(debug=True) 21 | -------------------------------------------------------------------------------- /examples/vulnerable_code_across_files/absolute_from_file_command_injection_2.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from flask import Flask, render_template, request 3 | 4 | from other_file import return_the_arg 5 | 6 | 7 | app = Flask(__name__) 8 | 9 | @app.route('/menu', methods=['POST']) 10 | def menu(): 11 | param = request.form['suggestion'] 12 | 13 | command = return_the_arg('echo ' + param + ' >> ' + 'menu.txt') 14 | subprocess.call(command, shell=True) 15 | 16 | with open('menu.txt','r') as f: 17 | menu = f.read() 18 | 19 | return render_template('command_injection.html', menu=menu) 20 | 21 | if __name__ == '__main__': 22 | app.run(debug=True) 23 | -------------------------------------------------------------------------------- /examples/vulnerable_code_across_files/blackbox_library_call.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from flask import Flask, render_template, request 3 | 4 | # This is a lib we can't possibly see inside of 5 | import scrypt 6 | 7 | 8 | app = Flask(__name__) 9 | 10 | @app.route('/menu', methods=['GET']) 11 | def menu(): 12 | param = request.args.get('suggestion') 13 | 14 | # This is a function we can't possibly see inside of 15 | command = scrypt.encrypt('echo ' + param + ' >> ' + 'menu.txt', 'password') 16 | hey = command 17 | subprocess.call(hey, shell=True) 18 | 19 | with open('menu.txt','r') as f: 20 | menu = f.read() 21 | 22 | return render_template('command_injection.html', menu=menu) 23 | 24 | if __name__ == '__main__': 25 | app.run(debug=True) 26 | -------------------------------------------------------------------------------- /examples/vulnerable_code_across_files/import_file_command_injection.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, render_template, request 2 | 3 | import other_file 4 | 5 | 6 | app = Flask(__name__) 7 | 8 | @app.route('/menu', methods=['POST']) 9 | def menu(): 10 | param = request.form['suggestion'] 11 | 12 | other_file.shell_the_arg('echo ' + param + ' >> ' + 'menu.txt') 13 | 14 | with open('menu.txt','r') as f: 15 | menu = f.read() 16 | 17 | return render_template('command_injection.html', menu=menu) 18 | 19 | if __name__ == '__main__': 20 | app.run(debug=True) 21 | -------------------------------------------------------------------------------- /examples/vulnerable_code_across_files/import_file_command_injection_2.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from flask import Flask, render_template, request 3 | 4 | import other_file 5 | 6 | 7 | app = Flask(__name__) 8 | 9 | @app.route('/menu', methods=['POST']) 10 | def menu(): 11 | param = request.form['suggestion'] 12 | 13 | command = other_file.return_the_arg('echo ' + param + ' >> ' + 'menu.txt') 14 | subprocess.call(command, shell=True) 15 | 16 | with open('menu.txt','r') as f: 17 | menu = f.read() 18 | 19 | return render_template('command_injection.html', menu=menu) 20 | 21 | if __name__ == '__main__': 22 | app.run(debug=True) 23 | -------------------------------------------------------------------------------- /examples/vulnerable_code_across_files/import_file_does_not_exist.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from flask import Flask, render_template, request 3 | 4 | import other_file 5 | 6 | 7 | app = Flask(__name__) 8 | 9 | @app.route('/menu', methods=['POST']) 10 | def menu(): 11 | param = request.form['suggestion'] 12 | 13 | command = other_file.does_not_exist('echo ' + param + ' >> ' + 'menu.txt') 14 | subprocess.call(command, shell=True) 15 | 16 | with open('menu.txt','r') as f: 17 | menu = f.read() 18 | 19 | return render_template('command_injection.html', menu=menu) 20 | 21 | if __name__ == '__main__': 22 | app.run(debug=True) 23 | -------------------------------------------------------------------------------- /examples/vulnerable_code_across_files/no_false_positive_absolute_from_file_command_injection_3.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from flask import Flask, render_template, request 3 | 4 | from other_file import return_constant_string 5 | 6 | 7 | app = Flask(__name__) 8 | 9 | @app.route('/menu', methods=['POST']) 10 | def menu(): 11 | param = request.form['suggestion'] 12 | 13 | command = return_constant_string('echo ' + param + ' >> ' + 'menu.txt') 14 | subprocess.call(command, shell=True) 15 | 16 | with open('menu.txt','r') as f: 17 | menu = f.read() 18 | 19 | return render_template('command_injection.html', menu=menu) 20 | 21 | if __name__ == '__main__': 22 | app.run(debug=True) 23 | -------------------------------------------------------------------------------- /examples/vulnerable_code_across_files/no_false_positive_import_file_command_injection_3.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | from flask import Flask, render_template, request 3 | 4 | import other_file 5 | 6 | 7 | app = Flask(__name__) 8 | 9 | @app.route('/menu', methods=['POST']) 10 | def menu(): 11 | param = request.form['suggestion'] 12 | 13 | command = other_file.return_constant_string('echo ' + param + ' >> ' + 'menu.txt') 14 | subprocess.call(command, shell=True) 15 | 16 | with open('menu.txt','r') as f: 17 | menu = f.read() 18 | 19 | return render_template('command_injection.html', menu=menu) 20 | 21 | if __name__ == '__main__': 22 | app.run(debug=True) 23 | -------------------------------------------------------------------------------- /examples/vulnerable_code_across_files/other_file.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | def return_constant_string(easy_to_find_in_logs): 4 | no_vuln = "This is not a vuln" 5 | return no_vuln 6 | 7 | def return_the_arg(easy_to_find_in_logs): 8 | hehe = 'bar' + easy_to_find_in_logs 9 | return hehe 10 | 11 | def shell_the_arg(easy_to_find_in_logs): 12 | subprocess.call(easy_to_find_in_logs, shell=True) 13 | -------------------------------------------------------------------------------- /pyt/README.rst: -------------------------------------------------------------------------------- 1 | How It Works 2 | ============ 3 | 4 | `__main__.py`_ is where all the high-level steps happen. 5 | 6 | .. _\_\_main\_\_.py: https://github.com/python-security/pyt/blob/master/pyt/__main__.py 7 | 8 | Step 1 9 | Parse command line arguments. 10 | 11 | `parse_args`_ in `usage.py`_ 12 | 13 | .. _parse_args: https://github.com/python-security/pyt/blob/re_organize_code/pyt/usage.py#L113 14 | .. _usage.py: https://github.com/python-security/pyt/blob/master/pyt/usage.py 15 | 16 | 17 | Step 2 18 | Generate the `Abstract Syntax Tree (AST)`_. 19 | 20 | Essentially done in these lines of code with the `ast`_ module: 21 | 22 | .. code-block:: python 23 | 24 | import ast 25 | ast.parse(f.read()) 26 | 27 | `generate_ast`_ in `ast_helper.py`_ 28 | 29 | .. _Abstract Syntax Tree (AST): https://en.wikipedia.org/wiki/Abstract_syntax_tree 30 | .. _ast: https://docs.python.org/3/library/ast.html 31 | .. _generate_ast: https://github.com/python-security/pyt/blob/re_organize_code/pyt/core/ast_helper.py#L24 32 | .. _ast_helper.py: https://github.com/python-security/pyt/blob/re_organize_code/pyt/core/ast_helper.py 33 | 34 | 35 | Step 3 36 | Pass the AST to create a `Control Flow Graph (CFG)`_ 37 | 38 | .. _Control Flow Graph (CFG): https://github.com/python-security/pyt/tree/master/pyt/cfg 39 | 40 | Step 4 41 | Pass the CFG to a `Framework Adaptor`_, which will mark the arguments of certain functions as tainted sources. 42 | 43 | .. _Framework Adaptor: https://github.com/python-security/pyt/tree/master/pyt/web_frameworks 44 | 45 | Step 5 46 | Perform `(modified-)reaching definitions analysis`_, to know where definitions reach. 47 | 48 | .. _\(modified\-\)reaching definitions analysis: https://github.com/python-security/pyt/tree/master/pyt/analysis#where-do-definitions-reach 49 | 50 | Step 6 51 | `Find vulnerabilities`_, by seeing where sources reach, and how. 52 | 53 | .. _Find vulnerabilities: https://github.com/python-security/pyt/tree/master/pyt/vulnerabilities 54 | 55 | Step 7 56 | `Remove already known vulnerabilities`_ if a `baseline`_ (JSON file of a previous run of PyT) is provided. 57 | 58 | .. _Remove already known vulnerabilities: https://github.com/python-security/pyt/blob/re_organize_code/pyt/vulnerabilities/vulnerability_helper.py#L194 59 | .. _baseline: https://github.com/python-security/pyt/blob/re_organize_code/pyt/usage.py#L54 60 | 61 | Step 8 62 | Output the results in either `text or JSON form`_, to stdout or the `output file`_. 63 | 64 | .. _text or JSON form: https://github.com/python-security/pyt/tree/master/pyt/formatters 65 | .. _output file: https://github.com/python-security/pyt/blob/re_organize_code/pyt/usage.py#L80 66 | 67 | Here is an image from the `original thesis`_: 68 | 69 | .. image:: https://github.com/KevinHock/rtdpyt/blob/master/docs/img/overview.png 70 | 71 | .. _original thesis: http://projekter.aau.dk/projekter/files/239563289/final.pdf#page=62 72 | -------------------------------------------------------------------------------- /pyt/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/pyt/__init__.py -------------------------------------------------------------------------------- /pyt/__main__.py: -------------------------------------------------------------------------------- 1 | """The comand line module of PyT.""" 2 | 3 | import logging 4 | import os 5 | import sys 6 | from collections import defaultdict 7 | 8 | from .analysis.constraint_table import initialize_constraint_table 9 | from .analysis.fixed_point import analyse 10 | from .cfg import make_cfg 11 | from .core.ast_helper import generate_ast 12 | from .core.project_handler import ( 13 | get_directory_modules, 14 | get_modules 15 | ) 16 | from .usage import parse_args 17 | from .vulnerabilities import ( 18 | find_vulnerabilities, 19 | get_vulnerabilities_not_in_baseline 20 | ) 21 | from .vulnerabilities.vulnerability_helper import SanitisedVulnerability 22 | from .web_frameworks import ( 23 | FrameworkAdaptor, 24 | is_django_view_function, 25 | is_flask_route_function, 26 | is_function, 27 | is_function_without_leading_ 28 | ) 29 | 30 | log = logging.getLogger(__name__) 31 | 32 | 33 | def discover_files(targets, excluded_files, recursive=False): 34 | included_files = list() 35 | excluded_list = excluded_files.split(",") 36 | for target in targets: 37 | if os.path.isdir(target): 38 | for root, _, files in os.walk(target): 39 | for file in files: 40 | if file.endswith('.py') and file not in excluded_list: 41 | fullpath = os.path.join(root, file) 42 | included_files.append(fullpath) 43 | log.debug('Discovered file: %s', fullpath) 44 | if not recursive: 45 | break 46 | else: 47 | if target not in excluded_list: 48 | included_files.append(target) 49 | log.debug('Discovered file: %s', target) 50 | return included_files 51 | 52 | 53 | def retrieve_nosec_lines( 54 | path 55 | ): 56 | file = open(path, 'r') 57 | lines = file.readlines() 58 | return set( 59 | lineno for 60 | (lineno, line) in enumerate(lines, start=1) 61 | if '#nosec' in line or '# nosec' in line 62 | ) 63 | 64 | 65 | def main(command_line_args=sys.argv[1:]): # noqa: C901 66 | args = parse_args(command_line_args) 67 | 68 | logging_level = ( 69 | logging.ERROR if not args.verbose else 70 | logging.WARN if args.verbose == 1 else 71 | logging.INFO if args.verbose == 2 else 72 | logging.DEBUG 73 | ) 74 | logging.basicConfig(level=logging_level, format='[%(levelname)s] %(name)s: %(message)s') 75 | 76 | files = discover_files( 77 | args.targets, 78 | args.excluded_paths, 79 | args.recursive 80 | ) 81 | 82 | nosec_lines = defaultdict(set) 83 | 84 | if args.project_root: 85 | directory = os.path.normpath(args.project_root) 86 | project_modules = get_modules(directory, prepend_module_root=args.prepend_module_root) 87 | 88 | cfg_list = list() 89 | for path in sorted(files): 90 | log.info("Processing %s", path) 91 | if not args.ignore_nosec: 92 | nosec_lines[path] = retrieve_nosec_lines(path) 93 | 94 | if not args.project_root: 95 | directory = os.path.dirname(path) 96 | project_modules = get_modules(directory, prepend_module_root=args.prepend_module_root) 97 | 98 | local_modules = get_directory_modules(directory) 99 | tree = generate_ast(path) 100 | 101 | cfg = make_cfg( 102 | tree, 103 | project_modules, 104 | local_modules, 105 | path, 106 | allow_local_directory_imports=args.allow_local_imports 107 | ) 108 | cfg_list = [cfg] 109 | 110 | framework_route_criteria = is_flask_route_function 111 | if args.adaptor: 112 | if args.adaptor.lower().startswith('e'): 113 | framework_route_criteria = is_function 114 | elif args.adaptor.lower().startswith('p'): 115 | framework_route_criteria = is_function_without_leading_ 116 | elif args.adaptor.lower().startswith('d'): 117 | framework_route_criteria = is_django_view_function 118 | 119 | # Add all the route functions to the cfg_list 120 | FrameworkAdaptor( 121 | cfg_list, 122 | project_modules, 123 | local_modules, 124 | framework_route_criteria 125 | ) 126 | 127 | initialize_constraint_table(cfg_list) 128 | log.info("Analysing") 129 | analyse(cfg_list) 130 | log.info("Finding vulnerabilities") 131 | vulnerabilities = find_vulnerabilities( 132 | cfg_list, 133 | args.blackbox_mapping_file, 134 | args.trigger_word_file, 135 | args.interactive, 136 | nosec_lines 137 | ) 138 | 139 | if args.baseline: 140 | vulnerabilities = get_vulnerabilities_not_in_baseline( 141 | vulnerabilities, 142 | args.baseline 143 | ) 144 | 145 | args.formatter.report(vulnerabilities, args.output_file, not args.only_unsanitised) 146 | 147 | has_unsanitised_vulnerabilities = any( 148 | not isinstance(v, SanitisedVulnerability) 149 | for v in vulnerabilities 150 | ) 151 | if has_unsanitised_vulnerabilities: 152 | sys.exit(1) 153 | 154 | 155 | if __name__ == '__main__': 156 | main() 157 | -------------------------------------------------------------------------------- /pyt/analysis/README.rst: -------------------------------------------------------------------------------- 1 | This code is responsible for answering two questions: 2 | 3 | 4 | Where do definitions reach? 5 | =========================== 6 | 7 | Traditionally `reaching definitions`_, a classic dataflow-analysis, 8 | has been used to answer this question. To understand reaching definitions, 9 | watch this `wonderful YouTube video`_ and come back here. 10 | We use reaching definitions, with one small modification, 11 | a `reassignment check`_. 12 | 13 | 14 | .. code-block:: python 15 | 16 | # Reassignment check 17 | if cfg_node.left_hand_side not in cfg_node.right_hand_side_variables: 18 | # Get previous assignments of cfg_node.left_hand_side and remove them from JOIN 19 | arrow_result = self.arrow(JOIN, cfg_node.left_hand_side) 20 | 21 | We do this because, e.g. 22 | 23 | .. code-block:: python 24 | 25 | image_name = request.args.get('image_name') 26 | image_name = os.path.join(base_dir, image_name) 27 | send_file(image_name) 28 | 29 | we still want to know that something from a request reached `send_file`. 30 | 31 | 32 | .. _reaching definitions: https://en.wikipedia.org/wiki/Reaching_definition 33 | .. _reassignment check: https://github.com/python-security/pyt/blob/re_organize_code/pyt/analysis/reaching_definitions_taint.py#L23-L26 34 | .. _wonderful YouTube video: https://www.youtube.com/watch?v=NVBQSR_HdL0 35 | 36 | 37 | How does a definition reach? 38 | ============================ 39 | 40 | After we know that a definition reaches a use that we are interested in, 41 | we use what are called `definition-use chains`_ to figure out how definitions 42 | reach their uses. This is necessary because there may be multiple paths from 43 | definition to use. Here is how we create `definition_chains`_: 44 | 45 | .. code-block:: python 46 | 47 | def build_def_use_chain( 48 | cfg_nodes, 49 | lattice 50 | ): 51 | def_use = defaultdict(list) 52 | # For every node 53 | for node in cfg_nodes: 54 | # That's a definition 55 | if isinstance(node, AssignmentNode): 56 | # Get the uses 57 | for variable in node.right_hand_side_variables: 58 | # Loop through most of the nodes before it 59 | for earlier_node in get_constraint_nodes(node, lattice): 60 | # and add them to the 'uses list' of each earlier node, when applicable 61 | # 'earlier node' here being a simplification 62 | if variable in earlier_node.left_hand_side: 63 | def_use[earlier_node].append(node) 64 | return def_use 65 | 66 | .. _definition-use chains: https://en.wikipedia.org/wiki/Use-define_chain 67 | .. _definition_chains: https://github.com/python-security/pyt/blob/re_organize_code/pyt/analysis/definition_chains.py#L16-L33 68 | 69 | 70 | Additional details 71 | ================== 72 | 73 | This folder will probably not change for the lifetime of the project, 74 | unless we were to implement more advanced analyses like `solving string 75 | constraints`_ or doing `alias analysis`_. Right now there are more 76 | pressing concerns, like handling web frameworks 77 | and handling all AST node types in the `CFG construction`_. 78 | 79 | Stefan and Bruno like the `Schwartzbach notes`_, as you will see in some comments. 80 | But looking up these two algorithms will yield countless results, my favorite is 81 | this `amazing guy from YouTube`_. 82 | 83 | 84 | .. _solving string constraints: https://zyh1121.github.io/z3str3Docs/inputLanguage.html 85 | .. _alias analysis: https://www3.cs.stonybrook.edu/~liu/papers/Alias-DLS10.pdf 86 | .. _CFG construction: https://github.com/python-security/pyt/tree/re_organize_code/pyt/cfg 87 | .. _Schwartzbach notes: http://lara.epfl.ch/w/_media/sav08:schwartzbach.pdf 88 | .. _amazing guy from YouTube: https://www.youtube.com/watch?v=NVBQSR_HdL0 89 | -------------------------------------------------------------------------------- /pyt/analysis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/pyt/analysis/__init__.py -------------------------------------------------------------------------------- /pyt/analysis/constraint_table.py: -------------------------------------------------------------------------------- 1 | """Global lookup table for constraints. 2 | 3 | Uses cfg node as key and operates on bitvectors in the form of ints.""" 4 | 5 | constraint_table = dict() 6 | 7 | 8 | def initialize_constraint_table(cfg_list): 9 | """Collects all given cfg nodes and initializes the table with value 0.""" 10 | for cfg in cfg_list: 11 | constraint_table.update(dict.fromkeys(cfg.nodes, 0)) 12 | 13 | 14 | def constraint_join(cfg_nodes): 15 | """Looks up all cfg_nodes and joins the bitvectors by using logical or.""" 16 | r = 0 17 | for e in cfg_nodes: 18 | r = r | constraint_table[e] 19 | return r 20 | -------------------------------------------------------------------------------- /pyt/analysis/definition_chains.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | from .constraint_table import constraint_table 4 | from ..core.node_types import AssignmentNode 5 | 6 | 7 | def get_constraint_nodes( 8 | node, 9 | lattice 10 | ): 11 | for n in lattice.get_elements(constraint_table[node]): 12 | if n is not node: 13 | yield n 14 | 15 | 16 | def build_def_use_chain( 17 | cfg_nodes, 18 | lattice 19 | ): 20 | def_use = defaultdict(list) 21 | # For every node 22 | for node in cfg_nodes: 23 | # That's a definition 24 | if isinstance(node, AssignmentNode): 25 | # Get the uses 26 | for variable in node.right_hand_side_variables: 27 | # Loop through most of the nodes before it 28 | for earlier_node in get_constraint_nodes(node, lattice): 29 | # and add them to the 'uses list' of each earlier node, when applicable 30 | # 'earlier node' here being a simplification 31 | if variable in earlier_node.left_hand_side: 32 | def_use[earlier_node].append(node) 33 | return def_use 34 | -------------------------------------------------------------------------------- /pyt/analysis/fixed_point.py: -------------------------------------------------------------------------------- 1 | """This module implements the fixed point algorithm.""" 2 | from .constraint_table import constraint_table 3 | from .reaching_definitions_taint import ReachingDefinitionsTaintAnalysis 4 | 5 | 6 | class FixedPointAnalysis(): 7 | """Run the fix point analysis.""" 8 | 9 | def __init__(self, cfg): 10 | """Fixed point analysis. 11 | 12 | Analysis must be a dataflow analysis containing a 'fixpointmethod' 13 | method that analyses one CFG.""" 14 | self.analysis = ReachingDefinitionsTaintAnalysis(cfg) 15 | self.cfg = cfg 16 | 17 | def fixpoint_runner(self): 18 | """Work list algorithm that runs the fixpoint algorithm.""" 19 | q = self.cfg.nodes 20 | 21 | while q != []: 22 | x_i = constraint_table[q[0]] # x_i = q[0].old_constraint 23 | self.analysis.fixpointmethod(q[0]) # y = F_i(x_1, ..., x_n); 24 | y = constraint_table[q[0]] # y = q[0].new_constraint 25 | 26 | if y != x_i: 27 | for node in self.analysis.dep(q[0]): # for (v in dep(v_i)) 28 | q.append(node) # q.append(v): 29 | constraint_table[q[0]] = y # q[0].old_constraint = q[0].new_constraint # x_i = y 30 | q = q[1:] # q = q.tail() # The list minus the head 31 | 32 | 33 | def analyse(cfg_list): 34 | """Analyse a list of control flow graphs with a given analysis type.""" 35 | for cfg in cfg_list: 36 | analysis = FixedPointAnalysis(cfg) 37 | analysis.fixpoint_runner() 38 | -------------------------------------------------------------------------------- /pyt/analysis/lattice.py: -------------------------------------------------------------------------------- 1 | from .constraint_table import constraint_table 2 | from ..core.node_types import AssignmentNode 3 | 4 | 5 | def get_lattice_elements(cfg_nodes): 6 | """Returns all assignment nodes as they are the only lattice elements 7 | in the reaching definitions analysis. 8 | """ 9 | for node in cfg_nodes: 10 | if isinstance(node, AssignmentNode): 11 | yield node 12 | 13 | 14 | class Lattice: 15 | def __init__(self, cfg_nodes): 16 | self.el2bv = dict() # Element to bitvector dictionary 17 | self.bv2el = list() # Bitvector to element list 18 | for i, e in enumerate(get_lattice_elements(cfg_nodes)): 19 | # Give each element a unique shift of 1 20 | self.el2bv[e] = 0b1 << i 21 | self.bv2el.insert(0, e) 22 | 23 | def get_elements(self, number): 24 | if number == 0: 25 | return [] 26 | 27 | elements = list() 28 | # Turn number into a binary string of length len(self.bv2el) 29 | binary_string = format(number, 30 | '0' + str(len(self.bv2el)) + 'b') 31 | for i, bit in enumerate(binary_string): 32 | if bit == '1': 33 | elements.append(self.bv2el[i]) 34 | return elements 35 | 36 | def in_constraint(self, node1, node2): 37 | """Checks if node1 is in node2's constraints 38 | For instance, if node1 = 010 and node2 = 110: 39 | 010 & 110 = 010 -> has the element.""" 40 | constraint = constraint_table[node2] 41 | if constraint == 0b0: 42 | return False 43 | 44 | try: 45 | value = self.el2bv[node1] 46 | except KeyError: 47 | return False 48 | 49 | return constraint & value != 0 50 | -------------------------------------------------------------------------------- /pyt/analysis/reaching_definitions_taint.py: -------------------------------------------------------------------------------- 1 | from .constraint_table import ( 2 | constraint_join, 3 | constraint_table 4 | ) 5 | from ..core.node_types import AssignmentNode 6 | from .lattice import Lattice 7 | 8 | 9 | class ReachingDefinitionsTaintAnalysis(): 10 | def __init__(self, cfg): 11 | self.cfg = cfg 12 | self.lattice = Lattice(cfg.nodes) 13 | 14 | def fixpointmethod(self, cfg_node): 15 | """The most important part of PyT, where we perform 16 | the variant of reaching definitions to find where sources reach. 17 | """ 18 | JOIN = self.join(cfg_node) 19 | # Assignment check 20 | if isinstance(cfg_node, AssignmentNode): 21 | arrow_result = JOIN 22 | 23 | # Reassignment check 24 | if cfg_node.left_hand_side not in cfg_node.right_hand_side_variables: 25 | # Get previous assignments of cfg_node.left_hand_side and remove them from JOIN 26 | arrow_result = self.arrow(JOIN, cfg_node.left_hand_side) 27 | 28 | arrow_result = arrow_result | self.lattice.el2bv[cfg_node] 29 | constraint_table[cfg_node] = arrow_result 30 | # Default case 31 | else: 32 | constraint_table[cfg_node] = JOIN 33 | 34 | def join(self, cfg_node): 35 | """Joins all constraints of the ingoing nodes and returns them. 36 | This represents the JOIN auxiliary definition from Schwartzbach.""" 37 | return constraint_join(cfg_node.ingoing) 38 | 39 | def arrow(self, JOIN, _id): 40 | """Removes all previous assignments from JOIN that have the same left hand side. 41 | This represents the arrow id definition from Schwartzbach.""" 42 | r = JOIN 43 | for node in self.lattice.get_elements(JOIN): 44 | if node.left_hand_side == _id: 45 | r = r ^ self.lattice.el2bv[node] 46 | return r 47 | 48 | def dep(self, q_1): 49 | """Represents the dep mapping from Schwartzbach.""" 50 | for node in q_1.outgoing: 51 | yield node 52 | -------------------------------------------------------------------------------- /pyt/cfg/__init__.py: -------------------------------------------------------------------------------- 1 | from .make_cfg import make_cfg 2 | 3 | __all__ = ['make_cfg'] 4 | -------------------------------------------------------------------------------- /pyt/cfg/alias_helper.py: -------------------------------------------------------------------------------- 1 | """This module contains alias helper functions for the expr_visitor module.""" 2 | 3 | 4 | def as_alias_handler(alias_list): 5 | """Returns a list of all the names that will be called.""" 6 | list_ = list() 7 | for alias in alias_list: 8 | if alias.asname: 9 | list_.append(alias.asname) 10 | else: 11 | list_.append(alias.name) 12 | return list_ 13 | 14 | 15 | def handle_aliases_in_calls(name, import_alias_mapping): 16 | """Returns either None or the handled alias. 17 | Used in add_module. 18 | """ 19 | for key, val in import_alias_mapping.items(): 20 | # e.g. Foo == Foo 21 | # e.g. Foo.Bar startswith Foo. 22 | if name == key or \ 23 | name.startswith(key + '.'): 24 | 25 | # Replace key with val in name 26 | # e.g. StarbucksVisitor.Tea -> Eataly.Tea because 27 | # "from .nested_folder import StarbucksVisitor as Eataly" 28 | return name.replace(key, val) 29 | return None 30 | 31 | 32 | def handle_aliases_in_init_files(name, import_alias_mapping): 33 | """Returns either None or the handled alias. 34 | Used in add_module. 35 | """ 36 | for key, val in import_alias_mapping.items(): 37 | # e.g. Foo == Foo 38 | # e.g. Foo.Bar startswith Foo. 39 | if name == val or \ 40 | name.startswith(val + '.'): 41 | 42 | # Replace val with key in name 43 | # e.g. StarbucksVisitor.Tea -> Eataly.Tea because 44 | # "from .nested_folder import StarbucksVisitor as Eataly" 45 | return name.replace(val, key) 46 | return None 47 | 48 | 49 | def handle_fdid_aliases(module_or_package_name, import_alias_mapping): 50 | """Returns either None or the handled alias. 51 | Used in add_module. 52 | fdid means from directory import directory. 53 | """ 54 | for key, val in import_alias_mapping.items(): 55 | if module_or_package_name == val: 56 | return key 57 | return None 58 | 59 | 60 | def not_as_alias_handler(names_list): 61 | """Returns a list of names ignoring any aliases.""" 62 | list_ = list() 63 | for alias in names_list: 64 | list_.append(alias.name) 65 | return list_ 66 | 67 | 68 | def retrieve_import_alias_mapping(names_list): 69 | """Creates a dictionary mapping aliases to their respective name. 70 | import_alias_names is used in module_definitions.py and visit_Call""" 71 | import_alias_names = dict() 72 | 73 | for alias in names_list: 74 | if alias.asname: 75 | import_alias_names[alias.asname] = alias.name 76 | return import_alias_names 77 | 78 | 79 | def fully_qualify_alias_labels(label, aliases): 80 | """Replace any aliases in label with the fully qualified name. 81 | 82 | Args: 83 | label -- A label : str representing a name (e.g. myos.system) 84 | aliases -- A dict of {alias: real_name} (e.g. {'myos': 'os'}) 85 | 86 | >>> fully_qualify_alias_labels('myos.mycall', {'myos':'os'}) 87 | 'os.mycall' 88 | """ 89 | for alias, full_name in aliases.items(): 90 | if label == alias: 91 | return full_name 92 | elif label.startswith(alias+'.'): 93 | return full_name + label[len(alias):] 94 | return label 95 | -------------------------------------------------------------------------------- /pyt/cfg/expr_visitor_helper.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | from ..core.node_types import ConnectToExitNode 4 | 5 | 6 | SavedVariable = namedtuple( 7 | 'SavedVariable', 8 | ( 9 | 'LHS', 10 | 'RHS' 11 | ) 12 | ) 13 | BUILTINS = ( 14 | 'get', 15 | 'Flask', 16 | 'run', 17 | 'replace', 18 | 'read', 19 | 'set_cookie', 20 | 'make_response', 21 | 'SQLAlchemy', 22 | 'Column', 23 | 'execute', 24 | 'sessionmaker', 25 | 'Session', 26 | 'filter', 27 | 'call', 28 | 'render_template', 29 | 'redirect', 30 | 'url_for', 31 | 'flash', 32 | 'jsonify' 33 | ) 34 | MUTATORS = ( # list.append(x) taints list if x is tainted 35 | 'add', 36 | 'append', 37 | 'extend', 38 | 'insert', 39 | 'update', 40 | ) 41 | 42 | 43 | def return_connection_handler(nodes, exit_node): 44 | """Connect all return statements to the Exit node.""" 45 | for function_body_node in nodes: 46 | if isinstance(function_body_node, ConnectToExitNode): 47 | if exit_node not in function_body_node.outgoing: 48 | function_body_node.connect(exit_node) 49 | -------------------------------------------------------------------------------- /pyt/cfg/make_cfg.py: -------------------------------------------------------------------------------- 1 | from .expr_visitor import ExprVisitor 2 | 3 | 4 | class CFG(): 5 | def __init__( 6 | self, 7 | nodes, 8 | blackbox_assignments, 9 | filename 10 | ): 11 | self.nodes = nodes 12 | self.blackbox_assignments = blackbox_assignments 13 | self.filename = filename 14 | 15 | def __repr__(self): 16 | output = '' 17 | for x, n in enumerate(self.nodes): 18 | output = ''.join((output, 'Node: ' + str(x) + ' ' + repr(n), '\n\n')) 19 | return output 20 | 21 | def __str__(self): 22 | output = '' 23 | for x, n in enumerate(self.nodes): 24 | output = ''.join((output, 'Node: ' + str(x) + ' ' + str(n), '\n\n')) 25 | return output 26 | 27 | 28 | def make_cfg( 29 | tree, 30 | project_modules, 31 | local_modules, 32 | filename, 33 | module_definitions=None, 34 | allow_local_directory_imports=True 35 | ): 36 | visitor = ExprVisitor( 37 | tree, 38 | project_modules, 39 | local_modules, 40 | filename, 41 | module_definitions, 42 | allow_local_directory_imports 43 | ) 44 | return CFG( 45 | visitor.nodes, 46 | visitor.blackbox_assignments, 47 | filename 48 | ) 49 | -------------------------------------------------------------------------------- /pyt/core/README.rst: -------------------------------------------------------------------------------- 1 | This directory contains miscellaneous code that is imported from different parts of the codebase. 2 | 3 | 4 | `ast_helper.py`_ contains 5 | 6 | 7 | 8 | - `generate_ast`_ to read any file and generate an AST from it, this is called from `__main__.py`_ and `stmt_visitor.py`_ when importing a module. 9 | 10 | - `get_call_names`_ used in `vars_visitor.py`_ when visiting a Subscript, and `framework_helper.py`_ on function decorators in `is_flask_route_function`_ 11 | 12 | - `get_call_names_as_string`_ used in `expr_visitor.py`_ to create ret_function_name as RHS and yld_function_name as LHS, and in stmt_visitor.py when connecting a function to a loop. 13 | 14 | - `Arguments`_ used in `expr_visitor.py`_ when processing the arguments of a user defined function and `framework_adaptor.py`_ to taint function definition arguments. 15 | 16 | 17 | .. _ast\_helper.py: https://github.com/python-security/pyt/blob/master/pyt/core/ast_helper.py 18 | .. _generate\_ast: https://github.com/python-security/pyt/blob/61ce4751531b01e968698aa537d58b68eb606f01/pyt/core/ast_helper.py#L24-L44 19 | 20 | .. _get\_call\_names: https://github.com/python-security/pyt/blob/b07035f2812817ed8340303cc8df9aeee3168489/pyt/core/ast_helper.py#L65-L68 21 | .. _get\_call\_names\_as\_string: https://github.com/python-security/pyt/blob/b07035f2812817ed8340303cc8df9aeee3168489/pyt/core/ast_helper.py#L76-L79 22 | .. _Arguments: https://github.com/python-security/pyt/blob/b07035f2812817ed8340303cc8df9aeee3168489/pyt/core/ast_helper.py#L81-L111 23 | 24 | 25 | 26 | `module_definitions.py`_ contains classes created mostly in `stmt_visitor.py`_ 27 | 28 | - `project_definitions`_ is a global dictionary modifed in the `append_if_local_or_in_imports`_ method of `ModuleDefinitions`_, read in `framework_adaptor.py`_ to `obtain all function nodes`_. 29 | 30 | - `ModuleDefinition`_ is created to keep track of parent definitions when visiting functions, classes and __init__.py files in `stmt_visitor.py`_ 31 | 32 | - `LocalModuleDefinition`_ is created when visiting functions and classes in `stmt_visitor.py`_ 33 | 34 | - `ModuleDefinitions`_ contains `append_if_local_or_in_imports`_ which is used in when adding a function or class to the module definitions in 35 | 36 | 37 | .. _obtain all function nodes: https://github.com/python-security/pyt/blob/02461063688fe02226e627c00adfb2c707d89aa0/pyt/web_frameworks/framework_adaptor.py#L93 38 | 39 | `node_types.py`_ contains all the different node types created in `expr_visitor.py`_ and `stmt_visitor.py`_ 40 | 41 | `project_handler.py`_ contains TODO 42 | 43 | .. _module_definitions.py: https://github.com/python-security/pyt/blob/master/pyt/core/module_definitions.py 44 | 45 | .. _node_types.py: https://github.com/python-security/pyt/blob/master/pyt/core/node_types.py 46 | 47 | .. _project_handler.py: https://github.com/python-security/pyt/blob/master/pyt/core/project_handler.py 48 | 49 | 50 | .. _\_\_main\_\_.py: https://github.com/python-security/pyt/blob/master/pyt/__main__.py 51 | .. _stmt\_visitor.py: https://github.com/python-security/pyt/blob/master/pyt/cfg/stmt_visitor.py 52 | .. _expr\_visitor.py: https://github.com/python-security/pyt/blob/master/pyt/cfg/expr_visitor.py 53 | .. _framework\_adaptor.py: https://github.com/python-security/pyt/tree/master/pyt/web_frameworks 54 | .. _framework\_helper.py: https://github.com/python-security/pyt/tree/master/pyt/web_frameworks 55 | .. _is\_flask\_route_function: https://github.com/python-security/pyt/tree/master/pyt/web_frameworks 56 | .. _vars\_visitor.py: https://github.com/python-security/pyt/tree/master/pyt/helper_visitors 57 | -------------------------------------------------------------------------------- /pyt/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/pyt/core/__init__.py -------------------------------------------------------------------------------- /pyt/core/ast_helper.py: -------------------------------------------------------------------------------- 1 | """This module contains helper function. 2 | Useful when working with the ast module.""" 3 | 4 | import ast 5 | import logging 6 | import os 7 | import subprocess 8 | from functools import lru_cache 9 | 10 | from .transformer import PytTransformer 11 | 12 | log = logging.getLogger(__name__) 13 | BLACK_LISTED_CALL_NAMES = ['self'] 14 | recursive = False 15 | 16 | 17 | def _convert_to_3(path): # pragma: no cover 18 | """Convert python 2 file to python 3.""" 19 | try: 20 | log.warn('##### Trying to convert %s to Python 3. #####', path) 21 | subprocess.call(['2to3', '-w', path]) 22 | except subprocess.SubprocessError: 23 | log.exception('Check if 2to3 is installed. https://docs.python.org/2/library/2to3.html') 24 | exit(1) 25 | 26 | 27 | @lru_cache() 28 | def generate_ast(path): 29 | """Generate an Abstract Syntax Tree using the ast module. 30 | 31 | Args: 32 | path(str): The path to the file e.g. example/foo/bar.py 33 | """ 34 | if os.path.isfile(path): 35 | with open(path, 'r') as f: 36 | try: 37 | tree = ast.parse(f.read()) 38 | return PytTransformer().visit(tree) 39 | except SyntaxError: # pragma: no cover 40 | global recursive 41 | if not recursive: 42 | _convert_to_3(path) 43 | recursive = True 44 | return generate_ast(path) 45 | else: 46 | raise SyntaxError('The ast module can not parse the file' 47 | ' and the python 2 to 3 conversion' 48 | ' also failed.') 49 | raise IOError('Input needs to be a file. Path: ' + path) 50 | 51 | 52 | def _get_call_names_helper(node): 53 | """Recursively finds all function names.""" 54 | if isinstance(node, ast.Name): 55 | if node.id not in BLACK_LISTED_CALL_NAMES: 56 | yield node.id 57 | elif isinstance(node, ast.Subscript): 58 | yield from _get_call_names_helper(node.value) 59 | elif isinstance(node, ast.Str): 60 | yield node.s 61 | elif isinstance(node, ast.Attribute): 62 | yield node.attr 63 | yield from _get_call_names_helper(node.value) 64 | 65 | 66 | def get_call_names(node): 67 | """Get a list of call names.""" 68 | return reversed(list(_get_call_names_helper(node))) 69 | 70 | 71 | def _list_to_dotted_string(list_of_components): 72 | """Convert a list to a string seperated by a dot.""" 73 | return '.'.join(list_of_components) 74 | 75 | 76 | def get_call_names_as_string(node): 77 | """Get a list of call names as a string.""" 78 | return _list_to_dotted_string(get_call_names(node)) 79 | 80 | 81 | class Arguments(): 82 | """Represents arguments of a function.""" 83 | 84 | def __init__(self, args): 85 | """Argument container class. 86 | 87 | Args: 88 | args(list(ast.args): The arguments in a function AST node. 89 | """ 90 | self.args = args.args 91 | self.varargs = args.vararg 92 | self.kwarg = args.kwarg 93 | self.kwonlyargs = args.kwonlyargs 94 | self.defaults = args.defaults 95 | self.kw_defaults = args.kw_defaults 96 | 97 | self.arguments = list() 98 | if self.args: 99 | self.arguments.extend([x.arg for x in self.args]) 100 | if self.varargs: 101 | self.arguments.extend(self.varargs.arg) 102 | if self.kwarg: 103 | self.arguments.extend(self.kwarg.arg) 104 | if self.kwonlyargs: 105 | self.arguments.extend([x.arg for x in self.kwonlyargs]) 106 | 107 | def __getitem__(self, key): 108 | return self.arguments.__getitem__(key) 109 | 110 | def __len__(self): 111 | return self.args.__len__() 112 | -------------------------------------------------------------------------------- /pyt/core/project_handler.py: -------------------------------------------------------------------------------- 1 | """Generates a list of CFGs from a path. 2 | 3 | The module finds all python modules and generates an ast for them. 4 | """ 5 | import os 6 | 7 | 8 | _local_modules = list() 9 | 10 | 11 | def get_directory_modules(directory): 12 | """Return a list containing tuples of 13 | e.g. ('__init__', 'example/import_test_project/__init__.py') 14 | """ 15 | if _local_modules and os.path.dirname(_local_modules[0][1]) == directory: 16 | return _local_modules 17 | 18 | if not os.path.isdir(directory): 19 | # example/import_test_project/A.py -> example/import_test_project 20 | directory = os.path.dirname(directory) 21 | 22 | if directory == '': 23 | return _local_modules 24 | 25 | for path in os.listdir(directory): 26 | if _is_python_file(path): 27 | # A.py -> A 28 | module_name = os.path.splitext(path)[0] 29 | _local_modules.append((module_name, os.path.join(directory, path))) 30 | 31 | return _local_modules 32 | 33 | 34 | def get_modules(path, prepend_module_root=True): 35 | """Return a list containing tuples of 36 | e.g. ('test_project.utils', 'example/test_project/utils.py') 37 | """ 38 | module_root = os.path.split(path)[1] 39 | modules = list() 40 | for root, directories, filenames in os.walk(path): 41 | for filename in filenames: 42 | if _is_python_file(filename): 43 | directory = os.path.dirname( 44 | os.path.realpath( 45 | os.path.join( 46 | root, 47 | filename 48 | ) 49 | ) 50 | ).split(module_root)[-1].replace( 51 | os.sep, # e.g. '/' 52 | '.' 53 | ) 54 | directory = directory.replace('.', '', 1) 55 | 56 | module_name_parts = [] 57 | if prepend_module_root: 58 | module_name_parts.append(module_root) 59 | if directory: 60 | module_name_parts.append(directory) 61 | 62 | if filename == '__init__.py': 63 | path = root 64 | else: 65 | module_name_parts.append(os.path.splitext(filename)[0]) 66 | path = os.path.join(root, filename) 67 | 68 | modules.append(('.'.join(module_name_parts), path)) 69 | 70 | return modules 71 | 72 | 73 | def _is_python_file(path): 74 | if os.path.splitext(path)[1] == '.py': 75 | return True 76 | return False 77 | -------------------------------------------------------------------------------- /pyt/formatters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/pyt/formatters/__init__.py -------------------------------------------------------------------------------- /pyt/formatters/json.py: -------------------------------------------------------------------------------- 1 | """This formatter outputs the issues in JSON.""" 2 | import json 3 | from datetime import datetime 4 | 5 | from ..vulnerabilities.vulnerability_helper import SanitisedVulnerability 6 | 7 | 8 | def report( 9 | vulnerabilities, 10 | fileobj, 11 | print_sanitised, 12 | ): 13 | """ 14 | Prints issues in JSON format. 15 | Args: 16 | vulnerabilities: list of vulnerabilities to report 17 | fileobj: The output file object, which may be sys.stdout 18 | """ 19 | TZ_AGNOSTIC_FORMAT = "%Y-%m-%dT%H:%M:%SZ" 20 | time_string = datetime.utcnow().strftime(TZ_AGNOSTIC_FORMAT) 21 | 22 | machine_output = { 23 | 'generated_at': time_string, 24 | 'vulnerabilities': [ 25 | vuln.as_dict() for vuln in vulnerabilities 26 | if print_sanitised or not isinstance(vuln, SanitisedVulnerability) 27 | ] 28 | } 29 | 30 | result = json.dumps( 31 | machine_output, 32 | indent=4 33 | ) 34 | 35 | with fileobj: 36 | fileobj.write(result) 37 | -------------------------------------------------------------------------------- /pyt/formatters/screen.py: -------------------------------------------------------------------------------- 1 | """This formatter outputs the issues as color-coded text.""" 2 | from ..vulnerabilities.vulnerability_helper import SanitisedVulnerability, UnknownVulnerability 3 | 4 | RESET = '\033[0m' 5 | BOLD = '\033[1m' 6 | UNDERLINE = '\033[4m' 7 | DANGER = '\033[31m' 8 | GOOD = '\033[32m' 9 | HIGHLIGHT = '\033[45;1m' 10 | RED_ON_WHITE = '\033[31m\033[107m' 11 | 12 | 13 | def color(string, color_string): 14 | return color_string + str(string) + RESET 15 | 16 | 17 | def report( 18 | vulnerabilities, 19 | fileobj, 20 | print_sanitised, 21 | ): 22 | """ 23 | Prints issues in color-coded text format. 24 | 25 | Args: 26 | vulnerabilities: list of vulnerabilities to report 27 | fileobj: The output file object, which may be sys.stdout 28 | """ 29 | n_vulnerabilities = len(vulnerabilities) 30 | unsanitised_vulnerabilities = [v for v in vulnerabilities if not isinstance(v, SanitisedVulnerability)] 31 | n_unsanitised = len(unsanitised_vulnerabilities) 32 | n_sanitised = n_vulnerabilities - n_unsanitised 33 | heading = "{} vulnerabilit{} found{}.\n".format( 34 | 'No' if n_unsanitised == 0 else n_unsanitised, 35 | 'y' if n_unsanitised == 1 else 'ies', 36 | " (plus {} sanitised)".format(n_sanitised) if n_sanitised else "", 37 | ) 38 | vulnerabilities_to_print = vulnerabilities if print_sanitised else unsanitised_vulnerabilities 39 | with fileobj: 40 | for i, vulnerability in enumerate(vulnerabilities_to_print, start=1): 41 | fileobj.write(vulnerability_to_str(i, vulnerability)) 42 | 43 | if n_unsanitised == 0: 44 | fileobj.write(color(heading, GOOD)) 45 | else: 46 | fileobj.write(color(heading, DANGER)) 47 | 48 | 49 | def vulnerability_to_str(i, vulnerability): 50 | lines = [] 51 | lines.append(color('Vulnerability {}'.format(i), UNDERLINE)) 52 | lines.append('File: {}'.format(color(vulnerability.source.path, BOLD))) 53 | lines.append( 54 | 'User input at line {}, source "{}":'.format( 55 | vulnerability.source.line_number, 56 | color(vulnerability.source_trigger_word, HIGHLIGHT), 57 | ) 58 | ) 59 | lines.append('\t{}'.format(color(vulnerability.source.label, RED_ON_WHITE))) 60 | if vulnerability.reassignment_nodes: 61 | previous_path = None 62 | lines.append('Reassigned in:') 63 | for node in vulnerability.reassignment_nodes: 64 | if node.path != previous_path: 65 | lines.append('\tFile: {}'.format(node.path)) 66 | previous_path = node.path 67 | label = node.label 68 | if ( 69 | isinstance(vulnerability, SanitisedVulnerability) and 70 | node.label == vulnerability.sanitiser.label 71 | ): 72 | label = color(label, GOOD) 73 | lines.append( 74 | '\t Line {}:\t{}'.format( 75 | node.line_number, 76 | label, 77 | ) 78 | ) 79 | if vulnerability.source.path != vulnerability.sink.path: 80 | lines.append('File: {}'.format(color(vulnerability.sink.path, BOLD))) 81 | lines.append( 82 | 'Reaches line {}, sink "{}"'.format( 83 | vulnerability.sink.line_number, 84 | color(vulnerability.sink_trigger_word, HIGHLIGHT), 85 | ) 86 | ) 87 | lines.append('\t{}'.format( 88 | color(vulnerability.sink.label, RED_ON_WHITE) 89 | )) 90 | if isinstance(vulnerability, SanitisedVulnerability): 91 | lines.append( 92 | 'This vulnerability is {}{} by {}'.format( 93 | color('potentially ', BOLD) if not vulnerability.confident else '', 94 | color('sanitised', GOOD), 95 | color(vulnerability.sanitiser.label, BOLD), 96 | ) 97 | ) 98 | elif isinstance(vulnerability, UnknownVulnerability): 99 | lines.append( 100 | 'This vulnerability is unknown due to "{}"'.format( 101 | color(vulnerability.unknown_assignment.label, BOLD), 102 | ) 103 | ) 104 | return '\n'.join(lines) + '\n\n' 105 | -------------------------------------------------------------------------------- /pyt/formatters/text.py: -------------------------------------------------------------------------------- 1 | """This formatter outputs the issues as plain text.""" 2 | from ..vulnerabilities.vulnerability_helper import SanitisedVulnerability 3 | 4 | 5 | def report( 6 | vulnerabilities, 7 | fileobj, 8 | print_sanitised, 9 | ): 10 | """ 11 | Prints issues in text format. 12 | 13 | Args: 14 | vulnerabilities: list of vulnerabilities to report 15 | fileobj: The output file object, which may be sys.stdout 16 | print_sanitised: Print just unsanitised vulnerabilities or sanitised vulnerabilities as well 17 | """ 18 | n_vulnerabilities = len(vulnerabilities) 19 | unsanitised_vulnerabilities = [v for v in vulnerabilities if not isinstance(v, SanitisedVulnerability)] 20 | n_unsanitised = len(unsanitised_vulnerabilities) 21 | n_sanitised = n_vulnerabilities - n_unsanitised 22 | heading = "{} vulnerabilit{} found{}{}\n".format( 23 | 'No' if n_unsanitised == 0 else n_unsanitised, 24 | 'y' if n_unsanitised == 1 else 'ies', 25 | " (plus {} sanitised)".format(n_sanitised) if n_sanitised else "", 26 | ':' if n_vulnerabilities else '.', 27 | ) 28 | vulnerabilities_to_print = vulnerabilities if print_sanitised else unsanitised_vulnerabilities 29 | with fileobj: 30 | fileobj.write(heading) 31 | 32 | for i, vulnerability in enumerate(vulnerabilities_to_print, start=1): 33 | fileobj.write('Vulnerability {}:\n{}\n\n'.format(i, vulnerability)) 34 | -------------------------------------------------------------------------------- /pyt/helper_visitors/README.rst: -------------------------------------------------------------------------------- 1 | Documentation coming soon. 2 | -------------------------------------------------------------------------------- /pyt/helper_visitors/__init__.py: -------------------------------------------------------------------------------- 1 | from .call_visitor import CallVisitor 2 | from .label_visitor import LabelVisitor 3 | from .right_hand_side_visitor import RHSVisitor 4 | from .vars_visitor import VarsVisitor 5 | 6 | 7 | __all__ = [ 8 | 'CallVisitor', 9 | 'LabelVisitor', 10 | 'RHSVisitor', 11 | 'VarsVisitor' 12 | ] 13 | -------------------------------------------------------------------------------- /pyt/helper_visitors/call_visitor.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import re 3 | from collections import defaultdict, namedtuple 4 | from itertools import count 5 | 6 | from ..core.ast_helper import get_call_names_as_string 7 | from .right_hand_side_visitor import RHSVisitor 8 | 9 | 10 | class CallVisitorResults( 11 | namedtuple( 12 | "CallVisitorResults", 13 | ("args", "kwargs", "unknown_args", "unknown_kwargs") 14 | ) 15 | ): 16 | __slots__ = () 17 | 18 | def all_results(self): 19 | for x in self.args: 20 | yield from x 21 | for x in self.kwargs.values(): 22 | yield from x 23 | yield from self.unknown_args 24 | yield from self.unknown_kwargs 25 | 26 | 27 | class CallVisitor(ast.NodeVisitor): 28 | def __init__(self, trigger_str): 29 | self.unknown_arg_visitor = RHSVisitor() 30 | self.unknown_kwarg_visitor = RHSVisitor() 31 | self.argument_visitors = defaultdict(lambda: RHSVisitor()) 32 | self._trigger_str = trigger_str 33 | 34 | def visit_Call(self, call_node): 35 | func_name = get_call_names_as_string(call_node.func) 36 | trigger_re = r"(^|\.){}$".format(re.escape(self._trigger_str)) 37 | if re.search(trigger_re, func_name): 38 | seen_starred = False 39 | for index, arg in enumerate(call_node.args): 40 | if isinstance(arg, ast.Starred): 41 | seen_starred = True 42 | if seen_starred: 43 | self.unknown_arg_visitor.visit(arg) 44 | else: 45 | self.argument_visitors[index].visit(arg) 46 | 47 | for keyword in call_node.keywords: 48 | if keyword.arg is None: 49 | self.unknown_kwarg_visitor.visit(keyword.value) 50 | else: 51 | self.argument_visitors[keyword.arg].visit(keyword.value) 52 | self.generic_visit(call_node) 53 | 54 | @classmethod 55 | def get_call_visit_results(cls, trigger_str, node): 56 | visitor = cls(trigger_str) 57 | visitor.visit(node) 58 | 59 | arg_results = [] 60 | for i in count(): 61 | try: 62 | arg_results.append(set(visitor.argument_visitors.pop(i).result)) 63 | except KeyError: 64 | break 65 | 66 | return CallVisitorResults( 67 | arg_results, 68 | {k: set(v.result) for k, v in visitor.argument_visitors.items()}, 69 | set(visitor.unknown_arg_visitor.result), 70 | set(visitor.unknown_kwarg_visitor.result), 71 | ) 72 | -------------------------------------------------------------------------------- /pyt/helper_visitors/right_hand_side_visitor.py: -------------------------------------------------------------------------------- 1 | """Contains a class that finds all names. 2 | Used to find all variables on a right hand side(RHS) of assignment. 3 | """ 4 | import ast 5 | 6 | 7 | class RHSVisitor(ast.NodeVisitor): 8 | """Visitor collecting all names.""" 9 | 10 | def __init__(self): 11 | """Initialize result as list.""" 12 | self.result = list() 13 | 14 | def visit_Name(self, node): 15 | self.result.append(node.id) 16 | 17 | def visit_Call(self, node): 18 | if node.args: 19 | for arg in node.args: 20 | self.visit(arg) 21 | if node.keywords: 22 | for keyword in node.keywords: 23 | self.visit(keyword) 24 | 25 | def visit_IfExp(self, node): 26 | # The test doesn't taint the assignment 27 | self.visit(node.body) 28 | self.visit(node.orelse) 29 | 30 | @classmethod 31 | def result_for_node(cls, node): 32 | visitor = cls() 33 | visitor.visit(node) 34 | return visitor.result 35 | -------------------------------------------------------------------------------- /pyt/usage.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | import sys 4 | 5 | from .formatters import json, screen, text 6 | 7 | 8 | default_blackbox_mapping_file = os.path.join( 9 | os.path.dirname(__file__), 10 | 'vulnerability_definitions', 11 | 'blackbox_mapping.json' 12 | ) 13 | 14 | 15 | default_trigger_word_file = os.path.join( 16 | os.path.dirname(__file__), 17 | 'vulnerability_definitions', 18 | 'all_trigger_words.pyt' 19 | ) 20 | 21 | 22 | def _add_required_group(parser): 23 | required_group = parser.add_argument_group('required arguments') 24 | required_group.add_argument( 25 | 'targets', metavar='targets', nargs='+', 26 | help='source file(s) or directory(s) to be scanned', 27 | type=str 28 | ) 29 | 30 | 31 | def _add_optional_group(parser): 32 | optional_group = parser.add_argument_group('optional arguments') 33 | optional_group.add_argument( 34 | '-v', '--verbose', 35 | action='count', 36 | help='Increase logging verbosity. Can repeated e.g. -vvv', 37 | ) 38 | optional_group.add_argument( 39 | '-a', '--adaptor', 40 | help='Choose a web framework adaptor: ' 41 | 'Flask(Default), Django, Every or Pylons', 42 | type=str 43 | ) 44 | optional_group.add_argument( 45 | '-pr', '--project-root', 46 | help='Add project root, only important when the entry ' 47 | 'file is not at the root of the project.', 48 | type=str 49 | ) 50 | optional_group.add_argument( 51 | '-b', '--baseline', 52 | help='Path of a baseline report to compare against ' 53 | '(only JSON-formatted files are accepted)', 54 | type=str, 55 | default=False, 56 | metavar='BASELINE_JSON_FILE', 57 | ) 58 | optional_group.add_argument( 59 | '-t', '--trigger-word-file', 60 | help='Input file with a list of sources and sinks', 61 | type=str, 62 | default=default_trigger_word_file 63 | ) 64 | optional_group.add_argument( 65 | '-m', '--blackbox-mapping-file', 66 | help='Input blackbox mapping file.', 67 | type=str, 68 | default=default_blackbox_mapping_file 69 | ) 70 | optional_group.add_argument( 71 | '-i', '--interactive', 72 | help='Will ask you about each blackbox function call in vulnerability chains.', 73 | action='store_true', 74 | default=False 75 | ) 76 | optional_group.add_argument( 77 | '-o', '--output', 78 | help='Write report to filename', 79 | dest='output_file', 80 | action='store', 81 | type=argparse.FileType('w'), 82 | default=sys.stdout, 83 | ) 84 | optional_group.add_argument( 85 | '--ignore-nosec', 86 | dest='ignore_nosec', 87 | action='store_true', 88 | help='Do not skip lines with # nosec comments' 89 | ) 90 | optional_group.add_argument( 91 | '-r', '--recursive', 92 | dest='recursive', 93 | action='store_true', 94 | help='Find and process files in subdirectories' 95 | ) 96 | optional_group.add_argument( 97 | '-x', '--exclude', 98 | dest='excluded_paths', 99 | action='store', 100 | default='', 101 | help='Separate files with commas' 102 | ) 103 | optional_group.add_argument( 104 | '--dont-prepend-root', 105 | help="In project root e.g. /app, imports are not prepended with app.*", 106 | action='store_false', 107 | default=True, 108 | dest='prepend_module_root' 109 | ) 110 | optional_group.add_argument( 111 | '--no-local-imports', 112 | help='If set, absolute imports must be relative to the project root. ' 113 | 'If not set, modules in the same directory can be imported just by their names.', 114 | action='store_false', 115 | default=True, 116 | dest='allow_local_imports' 117 | ) 118 | optional_group.add_argument( 119 | '-u', '--only-unsanitised', 120 | help="Don't print sanitised vulnerabilities.", 121 | action='store_true', 122 | default=False, 123 | ) 124 | parser.set_defaults(formatter=text) 125 | formatter_group = optional_group.add_mutually_exclusive_group() 126 | formatter_group.add_argument( 127 | '-j', '--json', 128 | help='Prints JSON instead of report.', 129 | action='store_const', 130 | const=json, 131 | dest='formatter', 132 | ) 133 | formatter_group.add_argument( 134 | '-s', '--screen', 135 | help='Prints colorful report.', 136 | action='store_const', 137 | const=screen, 138 | dest='formatter', 139 | ) 140 | 141 | 142 | def parse_args(args): 143 | if len(args) == 0: 144 | args.append('-h') 145 | parser = argparse.ArgumentParser(prog='python -m pyt') 146 | 147 | # Hack to in order to list required args above optional 148 | parser._action_groups.pop() 149 | 150 | _add_required_group(parser) 151 | _add_optional_group(parser) 152 | 153 | args = parser.parse_args(args) 154 | if args.targets is None: 155 | parser.error('The targets argument is required') 156 | return args 157 | -------------------------------------------------------------------------------- /pyt/vulnerabilities/README.rst: -------------------------------------------------------------------------------- 1 | `find_vulnerabilities`_ is what `__main__.py`_ calls, it takes a list of `CFGs`_ and returns a list of vulnerabilities. 2 | 3 | 4 | The first thing we do is `find all sources and sinks in the file`_, and then `loop through each pair of source and sink to see if a source reaches a sink`_. 5 | 6 | Once we obtain def-use chains, we `find all of the paths from source to sink`_. 7 | 8 | 9 | 10 | After we get each vulnerability chain, we see `how_vulnerable`_ it is 11 | 12 | There are a few different `vulnerability types`_ used in `how_vulnerable`_. 13 | 14 | .. _find_vulnerabilities: https://github.com/python-security/pyt/blob/re_organize_code/pyt/vulnerabilities/vulnerabilities.py#L467-L502 15 | .. _\_\_main\_\_.py: https://github.com/python-security/pyt/blob/re_organize_code/pyt/__main__.py#L33-L106 16 | .. _CFGs: https://github.com/python-security/pyt/tree/re_organize_code/pyt/cfg 17 | 18 | .. _loop through each pair of source and sink to see if a source reaches a sink: https://github.com/python-security/pyt/blob/re_organize_code/pyt/vulnerabilities/vulnerabilities.py#L452-L464 19 | .. _find all sources and sinks in the file: https://github.com/python-security/pyt/blob/re_organize_code/pyt/vulnerabilities/vulnerabilities.py#L29-L59 20 | 21 | .. _find all of the paths from source to sink: https://github.com/python-security/pyt/blob/re_organize_code/pyt/vulnerabilities/vulnerabilities.py#L397-L405 22 | 23 | .. _vulnerability types: https://github.com/python-security/pyt/blob/re_organize_code/pyt/vulnerabilities/vulnerability_helper.py#L8-L12 24 | 25 | .. _how_vulnerable: https://github.com/python-security/pyt/blob/re_organize_code/pyt/vulnerabilities/vulnerabilities.py#L266-L323 26 | 27 | Configuration 28 | ============= 29 | The hard-coded list of sources and sinks can be found in the `vulnerability_definitions`_ folder, currently `all_trigger_words.pyt`_ is used by default. 30 | 31 | .. _vulnerability_definitions: https://github.com/python-security/pyt/tree/re_organize_code/pyt/vulnerability_definitions 32 | .. _all_trigger_words.pyt: https://github.com/python-security/pyt/blob/re_organize_code/pyt/vulnerability_definitions/all_trigger_words.pyt 33 | 34 | Types of Vulnerabilities 35 | ======================== 36 | 37 | There are 3 kinds of vulnerabilities reported by PyT whose classes are defined in `vulnerability_helper.py`_: `regular`_, `sanitised`_ and `unknown`_. We report a `sanitised`_ vulnerability when there is a known sanitiser between the source and sink, with `confidence when the sanitiser is an assignment`_ and with `uncertainty if it is potentially sanitised by an if statement`_. Here is an example: 38 | 39 | .. _confidence when the sanitiser is an assignment: https://github.com/python-security/pyt/blob/re_organize_code/pyt/vulnerabilities/vulnerabilities.py#L293 40 | .. _uncertainty if it is potentially sanitised by an if statement: https://github.com/python-security/pyt/blob/re_organize_code/pyt/vulnerabilities/vulnerabilities.py#L394 41 | 42 | .. code-block:: python 43 | 44 | 5 @app.route('/XSS_param', methods =['GET']) 45 | 6 def XSS1(): 46 | 7 param = request.args.get('param', 'not set') 47 | 8 safe_param = Markup.escape(param) 48 | 9 html = open('templates/XSS_param.html').read() 49 | 10 resp = make_response(html.replace('{{ param }}', safe_param)) 50 | 11 return resp 51 | 52 | .. code-block:: python 53 | 54 | File: examples/vulnerable_code/XSS_sanitised.py 55 | > User input at line 7, source "request.args.get(": 56 | ~call_1 = ret_request.args.get('param', 'not set') 57 | Reassigned in: 58 | File: examples/vulnerable_code/XSS_sanitised.py 59 | > Line 7: param = ~call_1 60 | File: examples/vulnerable_code/XSS_sanitised.py 61 | > Line 8: ~call_2 = ret_Markup.escape(param) 62 | File: examples/vulnerable_code/XSS_sanitised.py 63 | > Line 8: safe_param = ~call_2 64 | File: examples/vulnerable_code/XSS_sanitised.py 65 | > reaches line 10, sink "replace(": 66 | ~call_5 = ret_html.replace('{{ param }}', safe_param) 67 | This vulnerability is sanitised by: Label: ~call_2 = ret_Markup.escape(param) 68 | 69 | and example code and output 70 | 71 | Unknown 72 | and example code and output 73 | 74 | 75 | How we find secondary nodes 76 | 77 | How we find sources/sinks 78 | 79 | How def-use chains are used 80 | h 81 | 82 | .. _vulnerability_helper.py: https://github.com/python-security/pyt/blob/re_organize_code/pyt/vulnerabilities/vulnerability_helper.py 83 | .. _regular: https://github.com/python-security/pyt/blob/re_organize_code/pyt/vulnerabilities/vulnerability_helper.py#L42-L91 84 | .. _sanitised: https://github.com/python-security/pyt/blob/re_organize_code/pyt/vulnerabilities/vulnerability_helper.py#L94-L119 85 | .. _unknown: https://github.com/python-security/pyt/blob/re_organize_code/pyt/vulnerabilities/vulnerability_helper.py#L122-L142 86 | -------------------------------------------------------------------------------- /pyt/vulnerabilities/__init__.py: -------------------------------------------------------------------------------- 1 | from .vulnerabilities import find_vulnerabilities 2 | from .vulnerability_helper import get_vulnerabilities_not_in_baseline 3 | 4 | 5 | __all__ = [ 6 | 'find_vulnerabilities', 7 | 'get_vulnerabilities_not_in_baseline' 8 | ] 9 | -------------------------------------------------------------------------------- /pyt/vulnerabilities/trigger_definitions_parser.py: -------------------------------------------------------------------------------- 1 | import json 2 | from collections import namedtuple 3 | 4 | 5 | Definitions = namedtuple( 6 | 'Definitions', 7 | ( 8 | 'sources', 9 | 'sinks' 10 | ) 11 | ) 12 | 13 | Source = namedtuple('Source', ('trigger_word')) 14 | 15 | 16 | class Sink: 17 | def __init__( 18 | self, trigger, *, 19 | unlisted_args_propagate=True, 20 | arg_dict=None, 21 | sanitisers=None, 22 | ): 23 | self._trigger = trigger 24 | self.sanitisers = sanitisers or [] 25 | self.arg_list_propagates = not unlisted_args_propagate 26 | 27 | if trigger[-1] != '(': 28 | if self.arg_list_propagates or arg_dict: 29 | raise ValueError("Propagation options specified, but trigger word isn't a function call") 30 | 31 | arg_dict = {} if arg_dict is None else arg_dict 32 | self.arg_position_to_kwarg = { 33 | position: name for name, position in arg_dict.items() if position is not None 34 | } 35 | self.kwarg_list = set(arg_dict.keys()) 36 | 37 | def arg_propagates(self, index): 38 | kwarg = self.get_kwarg_from_position(index) 39 | return self.kwarg_propagates(kwarg) 40 | 41 | def kwarg_propagates(self, keyword): 42 | in_list = keyword in self.kwarg_list 43 | return self.arg_list_propagates == in_list 44 | 45 | def get_kwarg_from_position(self, index): 46 | return self.arg_position_to_kwarg.get(index) 47 | 48 | @property 49 | def all_arguments_propagate_taint(self): 50 | if self.kwarg_list: 51 | return False 52 | return True 53 | 54 | @property 55 | def call(self): 56 | if self._trigger[-1] == '(': 57 | return self._trigger[:-1] 58 | return None 59 | 60 | @property 61 | def trigger_word(self): 62 | return self._trigger 63 | 64 | @classmethod 65 | def from_json(cls, key, data): 66 | return cls(trigger=key, **data) 67 | 68 | 69 | def parse(trigger_word_file): 70 | """Parse the file for source and sink definitions. 71 | 72 | Returns: 73 | A definitions tuple with sources and sinks. 74 | """ 75 | with open(trigger_word_file) as fd: 76 | triggers_dict = json.load(fd) 77 | sources = [Source(s) for s in triggers_dict['sources']] 78 | sinks = [ 79 | Sink.from_json(trigger, data) 80 | for trigger, data in triggers_dict['sinks'].items() 81 | ] 82 | return Definitions(sources, sinks) 83 | -------------------------------------------------------------------------------- /pyt/vulnerability_definitions/README.rst: -------------------------------------------------------------------------------- 1 | Documentation coming soon. 2 | -------------------------------------------------------------------------------- /pyt/vulnerability_definitions/all_trigger_words.pyt: -------------------------------------------------------------------------------- 1 | { 2 | "sources": [ 3 | "request.args.get(", 4 | "request.get_json(", 5 | "Markup(", 6 | "POST.get(", 7 | "GET.get(", 8 | "META.get(", 9 | "POST[", 10 | "GET[", 11 | "META[", 12 | "FILES[", 13 | ".data", 14 | "form[", 15 | "form(", 16 | "mark_safe(", 17 | "cookies[", 18 | "files[", 19 | "SQLAlchemy" 20 | ], 21 | "sinks": { 22 | "replace(": { 23 | "sanitisers": [ 24 | "escape" 25 | ] 26 | }, 27 | "send_file(": { 28 | "sanitisers": [ 29 | "'..'", 30 | "'..' in" 31 | ] 32 | }, 33 | "commands.getoutput(": {}, 34 | "commands.getstatusoutput(": {}, 35 | "eval(": {}, 36 | "exec(": {}, 37 | "execute(": {}, 38 | "filter(": {}, 39 | "flash(": {}, 40 | "jsonify(": {}, 41 | "os.execl(": {}, 42 | "os.execle(": {}, 43 | "os.execlp(": {}, 44 | "os.execlpe(": {}, 45 | "os.execv(": {}, 46 | "os.execve(": {}, 47 | "os.execvp(": {}, 48 | "os.execvpe(": {}, 49 | "os.popen(": {}, 50 | "os.popen2(": {}, 51 | "os.popen3(": {}, 52 | "os.popen4(": {}, 53 | "os.spawnl(": {}, 54 | "os.spawnle(": {}, 55 | "os.spawnlp(": {}, 56 | "os.spawnlpe(": {}, 57 | "os.spawnv(": {}, 58 | "os.spawnve(": {}, 59 | "os.spawnvp(": {}, 60 | "os.spawnvpe(": {}, 61 | "os.startfile(": {}, 62 | "os.system(": {}, 63 | "popen2.Popen3(": {}, 64 | "popen2.Popen4(": {}, 65 | "popen2.popen2(": {}, 66 | "popen2.popen3(": {}, 67 | "popen2.popen4(": {}, 68 | "redirect(": {}, 69 | "render(": {}, 70 | "render_template(": {}, 71 | "render_to_response(": {}, 72 | "set_cookie(": {}, 73 | "subprocess.Popen(": {}, 74 | "subprocess.call(": {}, 75 | "subprocess.check_call(": {}, 76 | "subprocess.check_output(": {}, 77 | "subprocess.run(": {}, 78 | "url_for(": {} 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /pyt/vulnerability_definitions/blackbox_mapping.json: -------------------------------------------------------------------------------- 1 | { 2 | "does_not_propagate": [ 3 | "fast_eddie", 4 | "url_for", 5 | "Post.query.paginate" 6 | ], 7 | "propagates": [ 8 | "os.path.join", 9 | "graham", 10 | "minnesota_fats" 11 | ] 12 | } -------------------------------------------------------------------------------- /pyt/vulnerability_definitions/django_trigger_words.pyt: -------------------------------------------------------------------------------- 1 | { 2 | "sources": [ 3 | "POST.get(", 4 | "GET.get(", 5 | "META.get(", 6 | "POST[", 7 | "GET[", 8 | "META[", 9 | "FILES[", 10 | ".data", 11 | "form[", 12 | "form(", 13 | "mark_safe(", 14 | "cookies[", 15 | "files[", 16 | "SQLAlchemy" 17 | ], 18 | "sinks": { 19 | "replace(": { 20 | "sanitisers": [ 21 | "escape" 22 | ] 23 | }, 24 | "send_file(": { 25 | "sanitisers": [ 26 | "'..'", 27 | "'..' in" 28 | ] 29 | }, 30 | "execute(": {}, 31 | "system(": {}, 32 | "filter(": {}, 33 | "subprocess.call(": {}, 34 | "render_template(": {}, 35 | "set_cookie(": {}, 36 | "redirect(": {}, 37 | "url_for(": {}, 38 | "flash(": {}, 39 | "jsonify(": {}, 40 | "render(": {}, 41 | "render_to_response(": {}, 42 | "Popen(": {} 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pyt/vulnerability_definitions/flask_trigger_words.pyt: -------------------------------------------------------------------------------- 1 | { 2 | "sources": [ 3 | "request.args.get(", 4 | "request.get_json(", 5 | ".data", 6 | "form[", 7 | "form(", 8 | "Markup(", 9 | "cookies[", 10 | "files[", 11 | "SQLAlchemy" 12 | ], 13 | "sinks": { 14 | "replace(": { 15 | "sanitisers": [ 16 | "escape" 17 | ] 18 | }, 19 | "send_file(": { 20 | "sanitisers": [ 21 | "'..'", 22 | "'..' in" 23 | ] 24 | }, 25 | "execute(": {}, 26 | "system(": {}, 27 | "filter(": {}, 28 | "subprocess.call(": {}, 29 | "render_template(": {}, 30 | "set_cookie(": {}, 31 | "redirect(": {}, 32 | "url_for(": {}, 33 | "flash(": {}, 34 | "jsonify(": {} 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pyt/vulnerability_definitions/test_positions.pyt: -------------------------------------------------------------------------------- 1 | { 2 | "sources": [ 3 | "request.args.get(", 4 | "make_taint(" 5 | ], 6 | "sinks": { 7 | "normal(": {}, 8 | "execute(": { 9 | "unlisted_args_propagate": false, 10 | "arg_dict": { 11 | "text": 0 12 | } 13 | }, 14 | "run(": { 15 | "arg_dict": { 16 | "non_propagating": 2, 17 | "something_else": 3 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pyt/vulnerability_definitions/test_triggers.pyt: -------------------------------------------------------------------------------- 1 | { 2 | "sources": [ 3 | "input" 4 | ], 5 | "sinks": { 6 | "eval(": { 7 | "sanitisers": [ 8 | "sanitise" 9 | ] 10 | }, 11 | "horse(": { 12 | "sanitisers": [ 13 | "japan", 14 | "host", 15 | "kost" 16 | ] 17 | }, 18 | "valmue": {} 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /pyt/web_frameworks/README.rst: -------------------------------------------------------------------------------- 1 | Web Frameworks 2 | ============== 3 | 4 | This code determines which functions have their arguments marked at tainted, for example by default the framework adaptor is Flask, so 5 | 6 | .. code-block:: python 7 | 8 | @app.route('/') 9 | def ito_en(image): 10 | 11 | will have arguments marked as tainted, whereas 12 | 13 | .. code-block:: python 14 | 15 | def tea(request, param): 16 | 17 | will not. (The ``--adaptor D`` option, for Django, would mark the 2nd functions' arguments as tainted and not the first.) 18 | 19 | There are currently 4 options for framework route criteria, in the `framework_helper.py`_ file: 20 | 21 | - `is_flask_route_function`_, the default, looks for a ``route`` decorator 22 | - `is_django_view_function`_, ``-a D``, looks if the first argument is named ``request`` 23 | - `is_function_without_leading_`_, ``-a P``, looks if the function does not start with an underscore 24 | - `is_function`_, ``-a E``, always returns True 25 | 26 | 27 | .. _framework_helper.py: https://github.com/python-security/pyt/blob/re_organize_code/pyt/web_frameworks/framework_helper.py 28 | 29 | .. _is\_django\_view\_function: https://github.com/python-security/pyt/blob/re_organize_code/pyt/web_frameworks/framework_helper.py#L7 30 | .. _is\_flask\_route\_function: https://github.com/python-security/pyt/blob/re_organize_code/pyt/web_frameworks/framework_helper.py#L14 31 | .. _is\_function\_without\_leading\_: https://github.com/python-security/pyt/blob/re_organize_code/pyt/web_frameworks/framework_helper.py#L28 32 | .. _is\_function: https://github.com/python-security/pyt/blob/re_organize_code/pyt/web_frameworks/framework_helper.py#L23 33 | 34 | 35 | How the Code Works 36 | ================== 37 | 38 | `FrameworkAdaptor`_ is what `__main__.py`_ creates, it takes a framework_route_criteria that is chosen by the --adaptor cli argument. The framework_route_criteria is a function that takes an `ast.FunctionDef`_ and returns whether or not it is a route in the selected web framework. 39 | 40 | We mark the arguments as tainted by `looping through them`_ and making them node type `TaintedNode`_, where we then `add them to the list of sources`_. 41 | 42 | 43 | .. _FrameworkAdaptor: https://github.com/python-security/pyt/blob/re_organize_code/pyt/web_frameworks/framework_adaptor.py#L14 44 | .. _\_\_main\_\_.py: https://github.com/python-security/pyt/blob/re_organize_code/pyt/__main__.py#L71-L85 45 | .. _ast.FunctionDef: http://greentreesnakes.readthedocs.io/en/latest/nodes.html#FunctionDef 46 | 47 | .. _looping through them: https://github.com/python-security/pyt/blob/re_organize_code/pyt/web_frameworks/framework_adaptor.py#L54 48 | .. _TaintedNode: https://github.com/python-security/pyt/blob/re_organize_code/pyt/core/node_types.py#L178 49 | .. _add them to the list of sources: https://github.com/python-security/pyt/blob/re_organize_code/pyt/vulnerabilities/vulnerabilities.py#L51 50 | 51 | Caveats 52 | ======= 53 | 54 | This currently is not smart enough to understand `class-based views`_, so you will have to use ``-a P`` to mark most functions arguments as tainted, and trim false-positives yourself, this is easier with the ``--baseline`` and ``--json`` options. 55 | 56 | .. _class-based views: http://flask.pocoo.org/docs/1.0/views/ 57 | -------------------------------------------------------------------------------- /pyt/web_frameworks/__init__.py: -------------------------------------------------------------------------------- 1 | from .framework_adaptor import ( 2 | FrameworkAdaptor, 3 | _get_func_nodes 4 | ) 5 | from .framework_helper import ( 6 | is_django_view_function, 7 | is_flask_route_function, 8 | is_function, 9 | is_function_without_leading_ 10 | ) 11 | 12 | 13 | __all__ = [ 14 | 'FrameworkAdaptor', 15 | 'is_django_view_function', 16 | 'is_flask_route_function', 17 | 'is_function', 18 | 'is_function_without_leading_', 19 | '_get_func_nodes' # Only used in framework_helper_test 20 | ] 21 | -------------------------------------------------------------------------------- /pyt/web_frameworks/framework_adaptor.py: -------------------------------------------------------------------------------- 1 | """A generic framework adaptor that leaves route criteria to the caller.""" 2 | 3 | import ast 4 | import logging 5 | 6 | from ..cfg import make_cfg 7 | from ..core.ast_helper import Arguments 8 | from ..core.module_definitions import project_definitions 9 | from ..core.node_types import ( 10 | AssignmentNode, 11 | TaintedNode 12 | ) 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | 17 | class FrameworkAdaptor(): 18 | """An engine that uses the template pattern to find all 19 | entry points in a framework and then taints their arguments. 20 | """ 21 | 22 | def __init__( 23 | self, 24 | cfg_list, 25 | project_modules, 26 | local_modules, 27 | is_route_function 28 | ): 29 | self.cfg_list = cfg_list 30 | self.project_modules = project_modules 31 | self.local_modules = local_modules 32 | self.is_route_function = is_route_function 33 | self.run() 34 | 35 | def get_func_cfg_with_tainted_args(self, definition): 36 | """Build a function cfg and return it, with all arguments tainted.""" 37 | log.debug("Getting CFG for %s", definition.name) 38 | func_cfg = make_cfg( 39 | definition.node, 40 | self.project_modules, 41 | self.local_modules, 42 | definition.path, 43 | definition.module_definitions 44 | ) 45 | 46 | args = Arguments(definition.node.args) 47 | if args: 48 | function_entry_node = func_cfg.nodes[0] 49 | function_entry_node.outgoing = list() 50 | first_node_after_args = func_cfg.nodes[1] 51 | first_node_after_args.ingoing = list() 52 | 53 | # We are just going to give all the tainted args the lineno of the def 54 | definition_lineno = definition.node.lineno 55 | 56 | # Taint all the arguments 57 | for i, arg in enumerate(args): 58 | node_type = TaintedNode 59 | if i == 0 and arg == 'self': 60 | node_type = AssignmentNode 61 | 62 | arg_node = node_type( 63 | label=arg, 64 | left_hand_side=arg, 65 | ast_node=None, 66 | right_hand_side_variables=[], 67 | line_number=definition_lineno, 68 | path=definition.path 69 | ) 70 | function_entry_node.connect(arg_node) 71 | # 1 and not 0 so that Entry Node remains first in the list 72 | func_cfg.nodes.insert(1, arg_node) 73 | arg_node.connect(first_node_after_args) 74 | 75 | return func_cfg 76 | 77 | def find_route_functions_taint_args(self): 78 | """Find all route functions and taint all of their arguments. 79 | 80 | Yields: 81 | CFG of each route function, with args marked as tainted. 82 | """ 83 | for definition in _get_func_nodes(): 84 | if self.is_route_function(definition.node): 85 | yield self.get_func_cfg_with_tainted_args(definition) 86 | 87 | def run(self): 88 | """Run find_route_functions_taint_args on each CFG.""" 89 | function_cfgs = list() 90 | for _ in self.cfg_list: 91 | function_cfgs.extend(self.find_route_functions_taint_args()) 92 | self.cfg_list.extend(function_cfgs) 93 | 94 | 95 | def _get_func_nodes(): 96 | """Get all function nodes.""" 97 | return [definition for definition in project_definitions.values() 98 | if isinstance(definition.node, ast.FunctionDef)] 99 | -------------------------------------------------------------------------------- /pyt/web_frameworks/framework_helper.py: -------------------------------------------------------------------------------- 1 | """Provides helper functions that help with determining if a function is a route function.""" 2 | import ast 3 | 4 | from ..core.ast_helper import get_call_names 5 | 6 | 7 | def is_django_view_function(ast_node): 8 | if len(ast_node.args.args): 9 | first_arg_name = ast_node.args.args[0].arg 10 | return first_arg_name == 'request' 11 | return False 12 | 13 | 14 | def is_flask_route_function(ast_node): 15 | """Check whether function uses a route decorator.""" 16 | for decorator in ast_node.decorator_list: 17 | if isinstance(decorator, ast.Call): 18 | if _get_last_of_iterable(get_call_names(decorator.func)) == 'route': 19 | return True 20 | return False 21 | 22 | 23 | def is_function(function): 24 | """Always returns true because arg is always a function.""" 25 | return True 26 | 27 | 28 | def is_function_without_leading_(ast_node): 29 | if ast_node.name.startswith('_'): 30 | return False 31 | return True 32 | 33 | 34 | def _get_last_of_iterable(iterable): 35 | """Get last element of iterable.""" 36 | item = None 37 | for item in iterable: 38 | pass 39 | return item 40 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.rst -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages 2 | from setuptools import setup 3 | 4 | 5 | VERSION = '0.42' 6 | 7 | 8 | setup( 9 | name='python-taint', 10 | packages=find_packages(exclude=(['tests*'])), 11 | version=VERSION, 12 | include_package_data=True, 13 | description='Find security vulnerabilities in Python web applications' 14 | ' using static analysis.', 15 | long_description="Check out PyT on `GitHub `_!", 16 | url='https://github.com/python-security/pyt', 17 | author='python-security', 18 | author_email='mr.thalmann@gmail.com', 19 | download_url='https://github.com/python-security/pyt/archive/{}.tar.gz'.format(VERSION), 20 | license='GPLv2', 21 | classifiers=[ 22 | 'Development Status :: 3 - Alpha', 23 | 'Intended Audience :: Developers', 24 | 'Intended Audience :: Science/Research', 25 | 'Topic :: Security', 26 | 'Topic :: Software Development', 27 | 'Topic :: Scientific/Engineering', 28 | 'Topic :: Utilities', 29 | 'License :: OSI Approved :: GNU General Public License v2 (GPLv2)', 30 | 'Programming Language :: Python :: 3.6' 31 | ], 32 | keywords=['security', 'vulnerability', 'web', 'flask', 'django', 'static-analysis', 'program-analysis'], 33 | install_requires=[], 34 | entry_points={ 35 | 'console_scripts': [ 36 | 'pyt = pyt.__main__:main' 37 | ] 38 | } 39 | ) 40 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/tests/__init__.py -------------------------------------------------------------------------------- /tests/__main__.py: -------------------------------------------------------------------------------- 1 | from unittest import ( 2 | TestLoader, 3 | TestSuite, 4 | TextTestRunner 5 | ) 6 | 7 | 8 | test_suite = TestSuite() 9 | loader = TestLoader() 10 | suite = loader.discover('.', pattern='*_test.py') 11 | 12 | runner = TextTestRunner(verbosity=2) 13 | result = runner.run(suite) 14 | 15 | if result.wasSuccessful(): 16 | print('Success') 17 | exit(0) 18 | else: # pragma: no cover 19 | print('Failure') 20 | exit(1) 21 | -------------------------------------------------------------------------------- /tests/analysis/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/tests/analysis/__init__.py -------------------------------------------------------------------------------- /tests/analysis/analysis_base_test_case.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | 3 | from ..base_test_case import BaseTestCase 4 | 5 | from pyt.analysis.constraint_table import ( 6 | constraint_table, 7 | initialize_constraint_table 8 | ) 9 | from pyt.analysis.fixed_point import FixedPointAnalysis 10 | from pyt.analysis.lattice import Lattice 11 | 12 | 13 | def clear_constraint_table(): 14 | for key in list(constraint_table): 15 | del constraint_table[key] 16 | 17 | 18 | class AnalysisBaseTestCase(BaseTestCase): 19 | connection = namedtuple( 20 | 'connection', 21 | ( 22 | 'constraintset', 23 | 'element' 24 | ) 25 | ) 26 | 27 | def setUp(self): 28 | self.cfg = None 29 | 30 | def assertInCfg(self, connections, lattice): 31 | """Assert that all connections in the connections list exists in the cfg, 32 | as well as all connections not in the list do not exist. 33 | 34 | Args: 35 | connections(list[tuples]): the node at index 0 of the tuple has 36 | to be in the new_constraint set of the node 37 | at index 1 of the tuple. 38 | lattice(Lattice): The lattice we're analysing. 39 | """ 40 | for connection in connections: 41 | self.assertEqual(lattice.in_constraint( 42 | self.cfg.nodes[connection[0]], 43 | self.cfg.nodes[connection[1]]), 44 | True, 45 | str(connection) + " expected to be connected") 46 | nodes = len(self.cfg.nodes) 47 | 48 | for element in range(nodes): 49 | for sets in range(nodes): 50 | if (element, sets) not in connections: 51 | self.assertEqual( 52 | lattice.in_constraint( 53 | self.cfg.nodes[element], 54 | self.cfg.nodes[sets] 55 | ), 56 | False, 57 | "(%s,%s)" % (self.cfg.nodes[element], self.cfg.nodes[sets]) + " expected to be disconnected" 58 | ) 59 | 60 | def constraints(self, list_of_constraints, node_number): 61 | for c in list_of_constraints: 62 | yield (c, node_number) 63 | 64 | def run_analysis(self, path): 65 | self.cfg_create_from_file(path) 66 | clear_constraint_table() 67 | initialize_constraint_table([self.cfg]) 68 | self.analysis = FixedPointAnalysis(self.cfg) 69 | self.analysis.fixpoint_runner() 70 | return Lattice(self.cfg.nodes) 71 | 72 | def string_compare_alnum(self, output, expected_string): 73 | return ( 74 | [char for char in output if char.isalnum()] == 75 | [char for char in expected_string if char.isalnum()] 76 | ) 77 | -------------------------------------------------------------------------------- /tests/base_test_case.py: -------------------------------------------------------------------------------- 1 | """A module that contains a base class that has helper methods for testing PyT.""" 2 | import unittest 3 | 4 | from pyt.cfg import make_cfg 5 | from pyt.core.ast_helper import generate_ast 6 | from pyt.core.module_definitions import project_definitions 7 | from pyt.core.transformer import PytTransformer 8 | 9 | 10 | class BaseTestCase(unittest.TestCase): 11 | """A base class that has helper methods for testing PyT.""" 12 | 13 | def assert_length(self, _list, *, expected_length, msg=None): 14 | actual_length = len(_list) 15 | self.assertEqual(expected_length, actual_length, msg=msg) 16 | 17 | def cfg_create_from_file( 18 | self, 19 | filename, 20 | project_modules=list(), 21 | local_modules=list() 22 | ): 23 | project_definitions.clear() 24 | tree = generate_ast(filename) 25 | self.cfg = make_cfg( 26 | tree, 27 | project_modules, 28 | local_modules, 29 | filename 30 | ) 31 | 32 | def cfg_create_from_ast( 33 | self, 34 | ast_tree, 35 | project_modules=list(), 36 | local_modules=list() 37 | ): 38 | project_definitions.clear() 39 | self.cfg = make_cfg( 40 | PytTransformer().visit(ast_tree), 41 | project_modules, 42 | local_modules, 43 | filename='?' 44 | ) 45 | -------------------------------------------------------------------------------- /tests/cfg/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/tests/cfg/__init__.py -------------------------------------------------------------------------------- /tests/cfg/cfg_base_test_case.py: -------------------------------------------------------------------------------- 1 | from ..base_test_case import BaseTestCase 2 | 3 | 4 | class CFGBaseTestCase(BaseTestCase): 5 | 6 | def assertInCfg(self, connections): 7 | """Asserts that all connections in the connections list exists in the cfg, 8 | as well as that all connections not in the list do not exist. 9 | 10 | Args: 11 | connections(list[tuple]): the node at index 0 of the tuple has 12 | to be in the new_constraint set of the node 13 | at index 1 of the tuple. 14 | """ 15 | for connection in connections: 16 | self.assertIn( 17 | self.cfg.nodes[connection[0]], 18 | self.cfg.nodes[connection[1]].outgoing, 19 | str(connection) + " expected to be connected" 20 | ) 21 | self.assertIn( 22 | self.cfg.nodes[connection[1]], 23 | self.cfg.nodes[connection[0]].ingoing, 24 | str(connection) + " expected to be connected" 25 | ) 26 | 27 | nodes = len(self.cfg.nodes) 28 | 29 | for element in range(nodes): 30 | for sets in range(nodes): 31 | if not (element, sets) in connections: 32 | self.assertNotIn( 33 | self.cfg.nodes[element], 34 | self.cfg.nodes[sets].outgoing, 35 | "(%s <- %s)" % (element, sets) + " expected to be disconnected" 36 | ) 37 | self.assertNotIn( 38 | self.cfg.nodes[sets], 39 | self.cfg.nodes[element].ingoing, 40 | "(%s <- %s)" % (sets, element) + " expected to be disconnected" 41 | ) 42 | 43 | def assertLineNumber(self, node, line_number): 44 | self.assertEqual(node.line_number, line_number) 45 | 46 | def cfg_list_to_dict(self, list): 47 | """This method converts the CFG list to a dict, making it easier to find nodes to test. 48 | This method assumes that no nodes in the code have the same label. 49 | """ 50 | return {x.label: x for x in list} 51 | -------------------------------------------------------------------------------- /tests/cfg/nested_functions_test.py: -------------------------------------------------------------------------------- 1 | import os.path 2 | 3 | from ..base_test_case import BaseTestCase 4 | from ..test_utils import get_modules_and_packages 5 | 6 | from pyt.core.project_handler import get_directory_modules 7 | 8 | 9 | class NestedTest(BaseTestCase): 10 | def test_nested_user_defined_function_calls(self): 11 | 12 | path = os.path.normpath('examples/nested_functions_code/nested_user_defined_function_calls.py') 13 | 14 | project_modules = get_modules_and_packages(os.path.dirname(path)) 15 | local_modules = get_directory_modules(os.path.dirname(path)) 16 | 17 | self.cfg_create_from_file(path, project_modules, local_modules) 18 | 19 | EXPECTED = ["Entry module", 20 | "foo = 'bar'", 21 | "save_1_foo = foo", 22 | "save_2_foo = foo", 23 | "temp_2_inner_arg = foo", 24 | "inner_arg = temp_2_inner_arg", 25 | "Function Entry inner", 26 | "inner_ret_val = inner_arg + 'hey'", 27 | "ret_inner = inner_ret_val", 28 | "Exit inner", 29 | "foo = save_2_foo", 30 | "~call_2 = ret_inner", 31 | "temp_1_outer_arg = ~call_2", 32 | "outer_arg = temp_1_outer_arg", 33 | "Function Entry outer", 34 | "outer_ret_val = outer_arg + 'hey'", 35 | "ret_outer = outer_ret_val", 36 | "Exit outer", 37 | "foo = save_1_foo", 38 | "~call_1 = ret_outer", 39 | "abc = ~call_1", 40 | "Exit module"] 41 | 42 | for node, expected_label in zip(self.cfg.nodes, EXPECTED): 43 | self.assertEqual(node.label, expected_label) 44 | -------------------------------------------------------------------------------- /tests/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/tests/core/__init__.py -------------------------------------------------------------------------------- /tests/core/transformer_test.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import unittest 3 | 4 | from pyt.core.transformer import PytTransformer 5 | 6 | 7 | class TransformerTest(unittest.TestCase): 8 | """Tests for the AsyncTransformer.""" 9 | 10 | def test_async_removed_by_transformer(self): 11 | self.maxDiff = 99999 12 | async_tree = ast.parse("\n".join([ 13 | "async def a():", 14 | " async for b in c():", 15 | " await b()", 16 | " async with d() as e:", 17 | " pass", 18 | " return await y()" 19 | ])) 20 | self.assertIsInstance(async_tree.body[0], ast.AsyncFunctionDef) 21 | self.assertIsInstance(async_tree.body[0].body[-1], ast.Return) 22 | self.assertIsInstance(async_tree.body[0].body[-1].value, ast.Await) 23 | 24 | sync_tree = ast.parse("\n".join([ 25 | "def a():", 26 | " for b in c():", 27 | " b()", 28 | " with d() as e:", 29 | " pass", 30 | " return y()" 31 | ])) 32 | self.assertIsInstance(sync_tree.body[0], ast.FunctionDef) 33 | 34 | transformed = PytTransformer().visit(async_tree) 35 | self.assertIsInstance(transformed.body[0], ast.FunctionDef) 36 | 37 | self.assertEqual(ast.dump(transformed), ast.dump(sync_tree)) 38 | 39 | def test_chained_function(self): 40 | chained_tree = ast.parse("\n".join([ 41 | "def a():", 42 | " b = c.d(e).f(g).h(i).j(k)", 43 | ])) 44 | 45 | separated_tree = ast.parse("\n".join([ 46 | "def a():", 47 | " __chain_tmp_3 = c.d(e)", 48 | " __chain_tmp_2 = __chain_tmp_3.f(g)", 49 | " __chain_tmp_1 = __chain_tmp_2.h(i)", 50 | " b = __chain_tmp_1.j(k)", 51 | ])) 52 | 53 | transformed = PytTransformer().visit(chained_tree) 54 | self.assertEqual(ast.dump(transformed), ast.dump(separated_tree)) 55 | 56 | def test_if_exp(self): 57 | complex_if_exp_tree = ast.parse("\n".join([ 58 | "def a():", 59 | " b = c if d.e(f) else g if h else i if j.k(l) else m", 60 | ])) 61 | 62 | separated_tree = ast.parse("\n".join([ 63 | "def a():", 64 | " __if_exp_0 = d.e(f)", 65 | " __if_exp_1 = j.k(l)", 66 | " b = c if __if_exp_0 else g if h else i if __if_exp_1 else m", 67 | ])) 68 | 69 | transformed = PytTransformer().visit(complex_if_exp_tree) 70 | self.assertEqual(ast.dump(transformed), ast.dump(separated_tree)) 71 | -------------------------------------------------------------------------------- /tests/helper_visitors/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/tests/helper_visitors/__init__.py -------------------------------------------------------------------------------- /tests/helper_visitors/call_visitor_test.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import unittest 3 | 4 | from pyt.helper_visitors import CallVisitor 5 | 6 | 7 | class CallVisitorTest(unittest.TestCase): 8 | def get_results(self, call_name, expr): 9 | tree = ast.parse(expr) 10 | return CallVisitor.get_call_visit_results(trigger_str=call_name, node=tree) 11 | 12 | def test_basic(self): 13 | call_args = self.get_results('func', 'func(a, b, x=c)') 14 | self.assertEqual(call_args.args, [{'a'}, {'b'}]) 15 | self.assertEqual(call_args.kwargs, {'x': {'c'}}) 16 | self.assertEqual(call_args.unknown_args, set()) 17 | self.assertEqual(call_args.unknown_kwargs, set()) 18 | 19 | def test_visits_each_argument_recursively(self): 20 | call_args = self.get_results('func', 'func(a + b, f(123), g(h(c=d)), e=i(123))') 21 | self.assertEqual(call_args.args, [{'a', 'b'}, set(), {'d'}]) 22 | self.assertEqual(call_args.kwargs, {'e': set()}) 23 | self.assertEqual(call_args.unknown_args, set()) 24 | self.assertEqual(call_args.unknown_kwargs, set()) 25 | 26 | def test_merge_when_function_called_inside_own_arguments(self): 27 | call_args = self.get_results('func', 'func(a + func(b, c, x=d), e)') 28 | self.assertEqual(call_args.args, [{'a', 'b', 'c', 'd'}, {'c', 'e'}]) 29 | self.assertEqual(call_args.kwargs, {'x': {'d'}}) 30 | self.assertEqual(call_args.unknown_args, set()) 31 | self.assertEqual(call_args.unknown_kwargs, set()) 32 | 33 | def test_star_args_kwargs(self): 34 | call_args = self.get_results('func', 'func(a, b, *c, *d, x=e, **f, **g)') 35 | self.assertEqual(call_args.args, [{'a'}, {'b'}]) 36 | self.assertEqual(call_args.kwargs, {'x': {'e'}}) 37 | self.assertEqual(call_args.unknown_args, {'c', 'd'}) 38 | self.assertEqual(call_args.unknown_kwargs, {'f', 'g'}) 39 | 40 | def test_call_inside_comprehension(self): 41 | call_args = self.get_results('func', '[row for row in db.func(a, b)]') 42 | self.assertEqual(call_args.args, [{'a'}, {'b'}]) 43 | self.assertEqual(call_args.kwargs, {}) 44 | self.assertEqual(call_args.unknown_args, set()) 45 | self.assertEqual(call_args.unknown_kwargs, set()) 46 | 47 | def test_call_inside_comprehension_2(self): 48 | call_args = self.get_results('func', '[func(a, b) for b in c]') 49 | self.assertEqual(call_args.args, [{'a'}, {'b'}]) 50 | self.assertEqual(call_args.kwargs, {}) 51 | self.assertEqual(call_args.unknown_args, set()) 52 | self.assertEqual(call_args.unknown_kwargs, set()) 53 | -------------------------------------------------------------------------------- /tests/helper_visitors/label_visitor_test.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import unittest 3 | 4 | from pyt.helper_visitors import LabelVisitor 5 | 6 | 7 | class LabelVisitorTestCase(unittest.TestCase): 8 | """Baseclass for LabelVisitor tests""" 9 | 10 | def perform_labeling_on_expression(self, expr): 11 | obj = ast.parse(expr) 12 | label = LabelVisitor() 13 | label.visit(obj) 14 | 15 | return label 16 | 17 | 18 | class LabelVisitorTest(LabelVisitorTestCase): 19 | def test_assign(self): 20 | label = self.perform_labeling_on_expression('a = 1') 21 | self.assertEqual(label.result, 'a = 1') 22 | 23 | def test_augassign(self): 24 | label = self.perform_labeling_on_expression('a +=2') 25 | self.assertEqual(label.result, 'a += 2') 26 | 27 | def test_compare_simple(self): 28 | label = self.perform_labeling_on_expression('a > b') 29 | self.assertEqual(label.result, 'a > b') 30 | 31 | def test_compare_multi(self): 32 | label = self.perform_labeling_on_expression('a > b > c') 33 | self.assertEqual(label.result, 'a > b > c') 34 | 35 | def test_binop(self): 36 | label = self.perform_labeling_on_expression('a / b') 37 | self.assertEqual(label.result, 'a / b') 38 | 39 | def test_call_no_arg(self): 40 | label = self.perform_labeling_on_expression('range()') 41 | self.assertEqual(label.result, 'range()') 42 | 43 | def test_call_single_arg(self): 44 | label = self.perform_labeling_on_expression('range(5)') 45 | self.assertEqual(label.result, 'range(5)') 46 | 47 | def test_call_multi_arg(self): 48 | label = self.perform_labeling_on_expression('range(1, 5)') 49 | self.assertEqual(label.result, 'range(1, 5)') 50 | 51 | def test_tuple_one_element(self): 52 | label = self.perform_labeling_on_expression('(1)') 53 | self.assertEqual(label.result, '1') 54 | 55 | def test_tuple_two_elements(self): 56 | label = self.perform_labeling_on_expression('(1, 2)') 57 | self.assertEqual(label.result, '(1, 2)') 58 | 59 | def test_empty_tuple(self): 60 | label = self.perform_labeling_on_expression('()') 61 | self.assertEqual(label.result, '()') 62 | 63 | def test_empty_list(self): 64 | label = self.perform_labeling_on_expression('[]') 65 | self.assertEqual(label.result, '[]') 66 | 67 | def test_list_one_element(self): 68 | label = self.perform_labeling_on_expression('[1]') 69 | self.assertEqual(label.result, '[1]') 70 | 71 | def test_list_two_elements(self): 72 | label = self.perform_labeling_on_expression('[1, 2]') 73 | self.assertEqual(label.result, '[1, 2]') 74 | 75 | def test_joined_str(self): 76 | label = self.perform_labeling_on_expression('f"a{f(b)}{c}d"') 77 | self.assertEqual(label.result, 'f\'a{f(b)}{c}d\'') 78 | 79 | def test_joined_str_with_format_spec(self): 80 | label = self.perform_labeling_on_expression('f"a{b!s:.{length}}"') 81 | self.assertEqual(label.result, 'f\'a{b!s:.{length}}\'') 82 | 83 | def test_starred(self): 84 | label = self.perform_labeling_on_expression('[a, *b] = *c, d') 85 | self.assertEqual(label.result, '[a, *b] = (*c, d)') 86 | 87 | def test_if_exp(self): 88 | label = self.perform_labeling_on_expression('a = b if c else d') 89 | self.assertEqual(label.result, 'a = (c) ? (b) : (d)') 90 | -------------------------------------------------------------------------------- /tests/main_test.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | from .base_test_case import BaseTestCase 4 | from pyt.__main__ import discover_files, main 5 | 6 | 7 | class MainTest(BaseTestCase): 8 | @mock.patch('pyt.__main__.discover_files') 9 | @mock.patch('pyt.__main__.parse_args') 10 | @mock.patch('pyt.__main__.find_vulnerabilities') 11 | @mock.patch('pyt.formatters.text') 12 | def test_text_output(self, mock_text, mock_find_vulnerabilities, mock_parse_args, mock_discover_files): 13 | mock_find_vulnerabilities.return_value = 'stuff' 14 | example_file = 'examples/vulnerable_code/inter_command_injection.py' 15 | output_file = 'mocked_outfile' 16 | 17 | import pyt.formatters.text 18 | mock_discover_files.return_value = [example_file] 19 | mock_parse_args.return_value = mock.Mock( 20 | project_root=None, 21 | baseline=None, 22 | formatter=pyt.formatters.text, 23 | output_file=output_file, 24 | only_unsanitised=False, 25 | ) 26 | with self.assertRaises(SystemExit): 27 | main(['parse_args is mocked']) 28 | assert mock_text.report.call_count == 1 29 | mock_text.report.assert_called_with( 30 | mock_find_vulnerabilities.return_value, 31 | mock_parse_args.return_value.output_file, 32 | True, 33 | ) 34 | 35 | @mock.patch('pyt.__main__.discover_files') 36 | @mock.patch('pyt.__main__.parse_args') 37 | @mock.patch('pyt.__main__.find_vulnerabilities') 38 | @mock.patch('pyt.formatters.text') 39 | def test_no_vulns_found(self, mock_text, mock_find_vulnerabilities, mock_parse_args, mock_discover_files): 40 | mock_find_vulnerabilities.return_value = [] 41 | example_file = 'examples/vulnerable_code/inter_command_injection.py' 42 | output_file = 'mocked_outfile' 43 | 44 | import pyt.formatters.text 45 | mock_discover_files.return_value = [example_file] 46 | mock_parse_args.return_value = mock.Mock( 47 | project_root=None, 48 | baseline=None, 49 | formatter=pyt.formatters.text, 50 | output_file=output_file, 51 | only_unsanitised=True, 52 | ) 53 | main(['parse_args is mocked']) # No SystemExit 54 | assert mock_text.report.call_count == 1 55 | mock_text.report.assert_called_with( 56 | mock_find_vulnerabilities.return_value, 57 | mock_parse_args.return_value.output_file, 58 | False, 59 | ) 60 | 61 | @mock.patch('pyt.__main__.discover_files') 62 | @mock.patch('pyt.__main__.parse_args') 63 | @mock.patch('pyt.__main__.find_vulnerabilities') 64 | @mock.patch('pyt.formatters.json') 65 | def test_json_output(self, mock_json, mock_find_vulnerabilities, mock_parse_args, mock_discover_files): 66 | mock_find_vulnerabilities.return_value = 'stuff' 67 | example_file = 'examples/vulnerable_code/inter_command_injection.py' 68 | output_file = 'mocked_outfile' 69 | 70 | import pyt.formatters.json 71 | mock_discover_files.return_value = [example_file] 72 | mock_parse_args.return_value = mock.Mock( 73 | project_root=None, 74 | baseline=None, 75 | formatter=pyt.formatters.json, 76 | output_file=output_file, 77 | only_unsanitised=False, 78 | ) 79 | with self.assertRaises(SystemExit): 80 | main(['parse_args is mocked']) 81 | assert mock_json.report.call_count == 1 82 | mock_json.report.assert_called_with( 83 | mock_find_vulnerabilities.return_value, 84 | mock_parse_args.return_value.output_file, 85 | True, 86 | ) 87 | 88 | 89 | class DiscoverFilesTest(BaseTestCase): 90 | def test_targets_with_no_excluded(self): 91 | targets = ["examples/vulnerable_code/inter_command_injection.py"] 92 | excluded_files = "" 93 | 94 | included_files = discover_files(targets, excluded_files) 95 | expected = ["examples/vulnerable_code/inter_command_injection.py"] 96 | self.assertListEqual(included_files, expected) 97 | 98 | def test_targets_with_exluded(self): 99 | targets = ["examples/vulnerable_code/inter_command_injection.py"] 100 | excluded_files = "examples/vulnerable_code/inter_command_injection.py" 101 | 102 | included_files = discover_files(targets, excluded_files) 103 | expected = [] 104 | self.assertListEqual(included_files, expected) 105 | 106 | def test_targets_with_recursive(self): 107 | targets = ["examples/vulnerable_code/"] 108 | excluded_files = "" 109 | 110 | included_files = discover_files(targets, excluded_files, True) 111 | self.assertEqual(len(included_files), 34) 112 | 113 | def test_targets_with_recursive_and_excluded(self): 114 | targets = ["examples/vulnerable_code/"] 115 | excluded_files = "inter_command_injection.py" 116 | 117 | included_files = discover_files(targets, excluded_files, True) 118 | self.assertEqual(len(included_files), 33) 119 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pyt.core.project_handler import _is_python_file 4 | 5 | 6 | def get_modules_and_packages(path): 7 | """Return a list containing tuples of 8 | e.g. ('folder', 'example/test_project/folder', '.folder') 9 | ('test_project.utils', 'example/test_project/utils.py') 10 | """ 11 | module_root = os.path.split(path)[1] 12 | modules = list() 13 | for root, directories, filenames in os.walk(path): 14 | for directory in directories: 15 | if directory != '__pycache__': 16 | full_path = os.path.join(root, directory) 17 | relative_path = os.path.realpath(full_path).split(module_root)[-1].replace(os.sep, '.') 18 | # Remove the dot in front to be consistent 19 | modules.append((relative_path[1:], full_path, relative_path)) 20 | 21 | for filename in filenames: 22 | if _is_python_file(filename): 23 | full_path = os.path.join(root, filename) 24 | directory = os.path.dirname(os.path.realpath(full_path)).split(module_root)[-1].replace(os.sep, '.') 25 | directory = directory.replace('.', '', 1) 26 | if directory: 27 | modules.append(('.'.join((module_root, directory, filename.replace('.py', ''))), full_path)) 28 | else: 29 | modules.append(('.'.join((module_root, filename.replace('.py', ''))), full_path)) 30 | 31 | return modules 32 | -------------------------------------------------------------------------------- /tests/usage_test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from contextlib import contextmanager 3 | from io import StringIO 4 | 5 | from .base_test_case import BaseTestCase 6 | from pyt.usage import parse_args 7 | 8 | 9 | @contextmanager 10 | def capture_sys_output(): 11 | capture_out, capture_err = StringIO(), StringIO() 12 | current_out, current_err = sys.stdout, sys.stderr 13 | try: 14 | sys.stdout, sys.stderr = capture_out, capture_err 15 | yield capture_out, capture_err 16 | finally: 17 | sys.stdout, sys.stderr = current_out, current_err 18 | 19 | 20 | class UsageTest(BaseTestCase): 21 | def test_no_args(self): 22 | with self.assertRaises(SystemExit): 23 | with capture_sys_output() as (stdout, _): 24 | parse_args([]) 25 | 26 | self.maxDiff = None 27 | 28 | EXPECTED = """usage: python -m pyt [-h] [-v] [-a ADAPTOR] [-pr PROJECT_ROOT] 29 | [-b BASELINE_JSON_FILE] [-t TRIGGER_WORD_FILE] 30 | [-m BLACKBOX_MAPPING_FILE] [-i] [-o OUTPUT_FILE] 31 | [--ignore-nosec] [-r] [-x EXCLUDED_PATHS] 32 | [--dont-prepend-root] [--no-local-imports] [-u] [-j | -s] 33 | targets [targets ...] 34 | 35 | required arguments: 36 | targets source file(s) or directory(s) to be scanned 37 | 38 | optional arguments: 39 | -v, --verbose Increase logging verbosity. Can repeated e.g. -vvv 40 | -a ADAPTOR, --adaptor ADAPTOR 41 | Choose a web framework adaptor: Flask(Default), 42 | Django, Every or Pylons 43 | -pr PROJECT_ROOT, --project-root PROJECT_ROOT 44 | Add project root, only important when the entry file 45 | is not at the root of the project. 46 | -b BASELINE_JSON_FILE, --baseline BASELINE_JSON_FILE 47 | Path of a baseline report to compare against (only 48 | JSON-formatted files are accepted) 49 | -t TRIGGER_WORD_FILE, --trigger-word-file TRIGGER_WORD_FILE 50 | Input file with a list of sources and sinks 51 | -m BLACKBOX_MAPPING_FILE, --blackbox-mapping-file BLACKBOX_MAPPING_FILE 52 | Input blackbox mapping file. 53 | -i, --interactive Will ask you about each blackbox function call in 54 | vulnerability chains. 55 | -o OUTPUT_FILE, --output OUTPUT_FILE 56 | Write report to filename 57 | --ignore-nosec Do not skip lines with # nosec comments 58 | -r, --recursive Find and process files in subdirectories 59 | -x EXCLUDED_PATHS, --exclude EXCLUDED_PATHS 60 | Separate files with commas 61 | --dont-prepend-root In project root e.g. /app, imports are not prepended 62 | with app.* 63 | --no-local-imports If set, absolute imports must be relative to the 64 | project root. If not set, modules in the same 65 | directory can be imported just by their names. 66 | -u, --only-unsanitised 67 | Don't print sanitised vulnerabilities. 68 | -j, --json Prints JSON instead of report. 69 | -s, --screen Prints colorful report.\n""" 70 | 71 | self.assertEqual(stdout.getvalue(), EXPECTED) 72 | 73 | def test_valid_args_but_no_targets(self): 74 | with self.assertRaises(SystemExit): 75 | with capture_sys_output() as (_, stderr): 76 | parse_args(['-j']) 77 | 78 | EXPECTED = """usage: python -m pyt [-h] [-v] [-a ADAPTOR] [-pr PROJECT_ROOT] 79 | [-b BASELINE_JSON_FILE] [-t TRIGGER_WORD_FILE] 80 | [-m BLACKBOX_MAPPING_FILE] [-i] [-o OUTPUT_FILE] 81 | [--ignore-nosec] [-r] [-x EXCLUDED_PATHS] 82 | [--dont-prepend-root] [--no-local-imports] [-u] [-j | -s] 83 | targets [targets ...] 84 | python -m pyt: error: the following arguments are required: targets\n""" 85 | 86 | self.assertEqual(stderr.getvalue(), EXPECTED) 87 | 88 | # def test_using_both_mutually_exclusive_args(self): 89 | # with self.assertRaises(SystemExit): 90 | # with capture_sys_output() as (_, stderr): 91 | # parse_args(['-f', 'foo.py', '-trim', '--interactive']) 92 | 93 | # EXPECTED = """usage: python -m pyt [-h] [-f FILEPATH] [-a ADAPTOR] [-pr PROJECT_ROOT] 94 | # [-b BASELINE_JSON_FILE] [-j] [-m BLACKBOX_MAPPING_FILE] 95 | # [-t TRIGGER_WORD_FILE] [-o OUTPUT_FILE] [-trim] [-i] 96 | # python -m pyt: error: argument -i/--interactive: not allowed with argument -trim/--trim-reassigned-in\n""" 97 | 98 | # self.assertEqual(stderr.getvalue(), EXPECTED) 99 | 100 | def test_normal_usage(self): 101 | with capture_sys_output() as (stdout, stderr): 102 | parse_args(['foo.py']) 103 | 104 | self.assertEqual(stdout.getvalue(), '') 105 | self.assertEqual(stderr.getvalue(), '') 106 | -------------------------------------------------------------------------------- /tests/vulnerabilities/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/tests/vulnerabilities/__init__.py -------------------------------------------------------------------------------- /tests/vulnerabilities/vulnerabilities_base_test_case.py: -------------------------------------------------------------------------------- 1 | from ..base_test_case import BaseTestCase 2 | 3 | 4 | class VulnerabilitiesBaseTestCase(BaseTestCase): 5 | 6 | def string_compare_alpha(self, output, expected_string): 7 | return ( 8 | [char for char in output if char.isalpha()] == 9 | [char for char in expected_string if char.isalpha()] 10 | ) 11 | 12 | def assertAlphaEqual(self, output, expected_string): 13 | self.assertEqual( 14 | ''.join(char for char in output if char.isalpha()), 15 | ''.join(char for char in expected_string if char.isalpha()) 16 | ) 17 | return True 18 | -------------------------------------------------------------------------------- /tests/web_frameworks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/python-security/pyt/f4ec9e127497a7ba7d08d68e8fca8b2f06756679/tests/web_frameworks/__init__.py -------------------------------------------------------------------------------- /tests/web_frameworks/framework_helper_test.py: -------------------------------------------------------------------------------- 1 | from ..base_test_case import BaseTestCase 2 | 3 | from pyt.web_frameworks import ( 4 | is_django_view_function, 5 | is_flask_route_function, 6 | is_function, 7 | is_function_without_leading_, 8 | _get_func_nodes 9 | ) 10 | 11 | 12 | class FrameworkEngineTest(BaseTestCase): 13 | def test_find_flask_functions(self): 14 | self.cfg_create_from_file('examples/example_inputs/django_flask_and_normal_functions.py') 15 | 16 | funcs = _get_func_nodes() 17 | 18 | i = 0 19 | for func in funcs: 20 | if is_flask_route_function(func.node): 21 | self.assertEqual(func.node.name, 'flask_function') 22 | i = i + 1 23 | # So it is supposed to be 1, because foo is not an app.route 24 | self.assertEqual(i, 1) 25 | 26 | def test_find_every_function_without_leading_underscore(self): 27 | self.cfg_create_from_file('examples/example_inputs/django_flask_and_normal_functions.py') 28 | 29 | funcs = _get_func_nodes() 30 | 31 | i = 0 32 | for func in funcs: 33 | if is_function_without_leading_(func.node): 34 | i = i + 1 35 | # So it is supposed to be 3, because we count all functions without a leading underscore 36 | self.assertEqual(i, 3) 37 | 38 | def test_find_every_function(self): 39 | self.cfg_create_from_file('examples/example_inputs/django_flask_and_normal_functions.py') 40 | 41 | funcs = _get_func_nodes() 42 | 43 | i = 0 44 | for func in funcs: 45 | if is_function(func.node): 46 | i = i + 1 47 | # So it is supposed to be 4, because we count all functions 48 | self.assertEqual(len(funcs), 4) 49 | 50 | def test_find_django_functions(self): 51 | self.cfg_create_from_file('examples/example_inputs/django_flask_and_normal_functions.py') 52 | 53 | funcs = _get_func_nodes() 54 | 55 | i = 0 56 | for func in funcs: 57 | if is_django_view_function(func.node): 58 | self.assertEqual(func.node.name, 'django_function') 59 | i = i + 1 60 | # So it is supposed to be 1 61 | self.assertEqual(i, 1) 62 | 63 | def test_find_django_views(self): 64 | self.cfg_create_from_file('examples/example_inputs/django_views.py') 65 | 66 | funcs = _get_func_nodes() 67 | 68 | i = 0 69 | for func in funcs: 70 | if is_django_view_function(func.node): 71 | self.assertIn('view_function', func.node.name) 72 | i = i + 1 73 | # So it is supposed to be 2 74 | self.assertEqual(i, 2) 75 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py36,py37,cover,lint 3 | 4 | [testenv] 5 | commands = 6 | python -m tests 7 | 8 | [testenv:cover] 9 | deps = 10 | coverage>=4.0,<4.4 11 | commands = 12 | coverage erase 13 | coverage run tests 14 | coverage report --include=tests/* --fail-under 100 15 | coverage report --include=pyt/* --fail-under 91 16 | 17 | [testenv:lint] 18 | deps = 19 | flake8 20 | pre-commit 21 | commands = 22 | pre-commit run 23 | flake8 . --count --exclude=examples,.env,venv,.tox --show-source --statistics --max-complexity=11 --max-line-length=127 --statistics 24 | --------------------------------------------------------------------------------