├── tests ├── __init__.py ├── test_helpers │ ├── __init__.py │ └── test_bad_builtin_use.py ├── test_twisted │ └── __init__.py ├── test_util.py ├── test_tree.py ├── test_bad_dl_use.py ├── test_bad_gl_use.py ├── test_bad_popen2_use.py ├── test_bad_shelve_use.py ├── test_bad_marshal_use.py ├── test_bad_commands_use.py ├── test_bad_eval_use.py ├── test_bad_tempfile_use.py ├── test_bad_compile_use.py ├── test_bad_xmlrpc_use.py ├── test_benchmark │ ├── conftest.py │ └── test_benchmark.py ├── test_bad_hashlib_use.py ├── test_bad_sys_use.py ├── test_bad_pycrypto_use.py ├── test_bad_pickle_use.py ├── test_bad_exec_use.py ├── test_bad_urllib3_module_attribute_use.py ├── test_bad_yaml_use.py ├── test_bad_subprocess_use.py ├── test_bad_urllib3_kwarg_use.py ├── test_bad_xml_use.py ├── test_bad_zipfile_use.py ├── test_bad_os_use.py ├── test_bad_requests_use.py ├── test_bad_ssl_module_attribute_use.py ├── test_bad_input_use.py ├── test_namespace.py ├── test_bad_tarfile_use.py ├── test_bad_onelogin_module_attribute_use.py ├── test_bad_defusedxml_use.py └── test_bad_xmlsec_module_attribute_use.py ├── dlint ├── linters │ ├── twisted │ │ ├── __init__.py │ │ ├── returnvalue_in_inlinecallbacks.py │ │ ├── inlinecallbacks_yield_statement.py │ │ └── yield_return_statement.py │ ├── helpers │ │ ├── __init__.py │ │ ├── bad_builtin_use.py │ │ ├── bad_module_use.py │ │ ├── bad_module_attribute_use.py │ │ ├── bad_kwarg_use.py │ │ └── bad_name_attribute_use.py │ ├── bad_popen2_use.py │ ├── bad_commands_use.py │ ├── bad_eval_use.py │ ├── bad_pycrypto_use.py │ ├── bad_hashlib_use.py │ ├── bad_dl_use.py │ ├── bad_gl_use.py │ ├── bad_urllib3_module_attribute_use.py │ ├── bad_shelve_use.py │ ├── bad_marshal_use.py │ ├── bad_zipfile_use.py │ ├── bad_xml_use.py │ ├── bad_yaml_use.py │ ├── bad_sys_use.py │ ├── bad_onelogin_module_attribute_use.py │ ├── bad_pickle_use.py │ ├── bad_tarfile_use.py │ ├── bad_exec_use.py │ ├── bad_compile_use.py │ ├── base.py │ ├── bad_tempfile_use.py │ ├── bad_os_use.py │ ├── bad_xmlsec_module_attribute_use.py │ ├── bad_cryptography_module_attribute_use.py │ ├── bad_xmlrpc_use.py │ ├── bad_ssl_module_attribute_use.py │ ├── bad_input_use.py │ ├── bad_subprocess_use.py │ ├── bad_random_generator_use.py │ ├── bad_requests_use.py │ ├── bad_duo_client_use.py │ ├── bad_urllib3_kwarg_use.py │ ├── bad_re_catastrophic_use.py │ ├── bad_itsdangerous_kwarg_use.py │ └── __init__.py ├── test │ ├── __init__.py │ └── base.py ├── redos │ ├── __init__.py │ └── __main__.py ├── util.py ├── __init__.py ├── multi.py ├── extension.py └── namespace.py ├── requirements.txt ├── .coveragerc ├── setup.cfg ├── .flake8 ├── requirements-dev.txt ├── .travis.yml ├── .appveyor.yml ├── docs └── linters │ ├── DUO118.md │ ├── DUO117.md │ ├── DUO124.md │ ├── DUO112.md │ ├── DUO115.md │ ├── DUO114.md │ ├── DUO131.md │ ├── DUO101.md │ ├── DUO107.md │ ├── DUO102.md │ ├── DUO104.md │ ├── DUO113.md │ ├── DUO126.md │ ├── DUO123.md │ ├── DUO127.md │ ├── DUO108.md │ ├── DUO130.md │ ├── DUO125.md │ ├── DUO111.md │ ├── DUO119.md │ ├── DUO120.md │ ├── DUO103.md │ ├── DUO105.md │ ├── DUO109.md │ ├── DUO121.md │ ├── DUO132.md │ ├── DUO106.md │ ├── DUO135.md │ ├── DUO110.md │ ├── DUO137.md │ ├── DUO128.md │ ├── DUO129.md │ ├── DUO116.md │ ├── DUO136.md │ ├── DUO122.md │ ├── DUO133.md │ ├── DUO134.md │ └── DUO138.md ├── .gitignore ├── LICENSE ├── setup.py └── CONTRIBUTING.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dlint/linters/twisted/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_helpers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_twisted/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flake8>=3.6.0,<4.0.0 2 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [report] 2 | include = dlint/* 3 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E501,W503 3 | include = dlint,tests 4 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest==4.6.9 2 | pytest-benchmark==3.2.3 3 | pytest-cov==2.8.1 4 | coveralls==1.11.0 5 | -------------------------------------------------------------------------------- /dlint/test/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | absolute_import, 3 | division, 4 | print_function, 5 | unicode_literals, 6 | ) 7 | 8 | from . import base # noqa F401 9 | -------------------------------------------------------------------------------- /dlint/redos/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | absolute_import, 3 | division, 4 | print_function, 5 | unicode_literals, 6 | ) 7 | 8 | from . import detect # noqa F401 9 | -------------------------------------------------------------------------------- /dlint/test/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import ast 11 | import textwrap 12 | import unittest 13 | 14 | 15 | class BaseTest(unittest.TestCase): 16 | 17 | @staticmethod 18 | def get_ast_node(s): 19 | return ast.parse(textwrap.dedent(s)) 20 | -------------------------------------------------------------------------------- /tests/test_util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestUtil(unittest.TestCase): 16 | 17 | def test_abc(self): 18 | assert dlint.util.ABC 19 | 20 | 21 | if __name__ == "__main__": 22 | unittest.main() 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | python: 4 | - 2.7 5 | - 3.5 6 | - 3.6 7 | - 3.7 8 | - 3.8-dev 9 | - nightly 10 | jobs: 11 | fast_finish: true 12 | allow_failures: 13 | - python: nightly 14 | install: 15 | - pip install --requirement requirements.txt 16 | - pip install --requirement requirements-dev.txt 17 | - pip install --editable . 18 | script: 19 | - flake8 20 | - flake8 --print-dlint-linters 21 | - pytest --cov 22 | after_success: 23 | - coveralls 24 | -------------------------------------------------------------------------------- /.appveyor.yml: -------------------------------------------------------------------------------- 1 | build: false 2 | version: "0.10.2.{build}" 3 | platform: "x64" 4 | environment: 5 | matrix: 6 | - PYTHON: "C:\\Python27" 7 | - PYTHON: "C:\\Python35" 8 | - PYTHON: "C:\\Python36" 9 | - PYTHON: "C:\\Python37" 10 | - PYTHON: "C:\\Python38" 11 | install: 12 | - pip install --requirement requirements.txt 13 | - pip install --requirement requirements-dev.txt 14 | - pip install --editable . 15 | test_script: 16 | - flake8 17 | - flake8 --print-dlint-linters 18 | - pytest --cov 19 | -------------------------------------------------------------------------------- /tests/test_tree.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import pytest 13 | 14 | import dlint 15 | 16 | 17 | class TestTree(unittest.TestCase): 18 | 19 | def test_decorator_name_unknown_type(self): 20 | unknown_type = None 21 | 22 | with pytest.raises(TypeError): 23 | dlint.tree.decorator_name(unknown_type) 24 | 25 | 26 | if __name__ == "__main__": 27 | unittest.main() 28 | -------------------------------------------------------------------------------- /docs/linters/DUO118.md: -------------------------------------------------------------------------------- 1 | # DUO118 2 | 3 | This linter looks for use of the `gl` module. 4 | 5 | From the documentation: 6 | 7 | > Some illegal calls to the GL library cause the Python interpreter to 8 | > dump core. In particular, the use of most GL calls is unsafe before the 9 | > first window is opened. 10 | 11 | ## Problematic code 12 | 13 | ```python 14 | import gl 15 | ``` 16 | 17 | ## Correct code 18 | 19 | None 20 | 21 | ## Rationale 22 | 23 | Crashes can lead to insecure behavior so we should avoid this module. 24 | 25 | ## Exceptions 26 | 27 | None 28 | -------------------------------------------------------------------------------- /dlint/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import abc 11 | import sys 12 | 13 | if sys.version_info >= (3, 4): 14 | ABC = abc.ABC 15 | else: 16 | ABC = abc.ABCMeta(str('ABC'), (), {}) 17 | 18 | 19 | def lstartswith(l1, l2): 20 | if len(l2) > len(l1): 21 | return False 22 | return l1[:len(l2)] == l2 23 | 24 | 25 | def lendswith(l1, l2): 26 | if len(l2) > len(l1): 27 | return False 28 | return l1[len(l1) - len(l2):] == l2 29 | -------------------------------------------------------------------------------- /docs/linters/DUO117.md: -------------------------------------------------------------------------------- 1 | # DUO117 2 | 3 | This linter looks for use of the `dl` module. 4 | 5 | From the documentation: 6 | 7 | > The dl module bypasses the Python type system and error handling. If 8 | > used incorrectly it may cause segmentation faults, crashes or other 9 | > incorrect behaviour. 10 | 11 | ## Problematic code 12 | 13 | ```python 14 | import dl 15 | ``` 16 | 17 | ## Correct code 18 | 19 | None - use the `ctypes` module instead 20 | 21 | ## Rationale 22 | 23 | Segmentation faults or crashes can lead to insecure behavior so we should 24 | avoid this module. 25 | 26 | ## Exceptions 27 | 28 | None 29 | -------------------------------------------------------------------------------- /dlint/linters/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | absolute_import, 3 | division, 4 | print_function, 5 | unicode_literals, 6 | ) 7 | 8 | from .bad_builtin_use import BadBuiltinUseLinter 9 | from .bad_kwarg_use import BadKwargUseLinter 10 | from .bad_module_use import BadModuleUseLinter 11 | from .bad_module_attribute_use import BadModuleAttributeUseLinter 12 | from .bad_name_attribute_use import BadNameAttributeUseLinter 13 | 14 | __all__ = [ 15 | 'BadBuiltinUseLinter', 16 | 'BadKwargUseLinter', 17 | 'BadModuleUseLinter', 18 | 'BadModuleAttributeUseLinter', 19 | 'BadNameAttributeUseLinter', 20 | ] 21 | -------------------------------------------------------------------------------- /dlint/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | absolute_import, 3 | division, 4 | print_function, 5 | unicode_literals, 6 | ) 7 | 8 | from . import linters # noqa F401 9 | from . import multi # noqa F401 10 | from . import namespace # noqa F401 11 | from . import redos # noqa F401 12 | from . import test # noqa F401 13 | from . import tree # noqa F401 14 | from . import util # noqa F401 15 | 16 | __name__ = 'dlint' 17 | __version__ = '0.10.2' 18 | __description__ = ( 19 | "Dlint is a tool for encouraging best coding practices " 20 | "and helping ensure Python code is secure." 21 | ) 22 | __url__ = 'https://github.com/dlint-py/dlint' 23 | __license__ = 'BSD-3-Clause' 24 | -------------------------------------------------------------------------------- /dlint/linters/bad_popen2_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_module_use 11 | 12 | 13 | class BadPopen2UseLinter(bad_module_use.BadModuleUseLinter): 14 | """This linter looks for use of the "popen2" module. This module execute 15 | shell commands, which often leads to arbitrary code execution bugs. 16 | """ 17 | off_by_default = False 18 | 19 | _code = 'DUO126' 20 | _error_tmpl = 'DUO126 avoid "popen2" module use' 21 | 22 | @property 23 | def illegal_modules(self): 24 | return [ 25 | "popen2", 26 | ] 27 | -------------------------------------------------------------------------------- /dlint/linters/bad_commands_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_module_use 11 | 12 | 13 | class BadCommandsUseLinter(bad_module_use.BadModuleUseLinter): 14 | """This linter looks for use of the "commands" module. This module execute 15 | shell commands, which often leads to arbitrary code execution bugs. 16 | """ 17 | off_by_default = False 18 | 19 | _code = 'DUO125' 20 | _error_tmpl = 'DUO125 avoid "commands" module use' 21 | 22 | @property 23 | def illegal_modules(self): 24 | return [ 25 | "commands", 26 | ] 27 | -------------------------------------------------------------------------------- /dlint/linters/bad_eval_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_builtin_use 11 | 12 | 13 | class BadEvalUseLinter(bad_builtin_use.BadBuiltinUseLinter): 14 | """This linter looks for use of the Python "eval" function. This function 15 | makes it far too easy to achieve arbitrary code execution, so we shouldn't 16 | support it in any context. 17 | """ 18 | off_by_default = False 19 | 20 | _code = 'DUO104' 21 | _error_tmpl = 'DUO104 use of "eval" is insecure' 22 | 23 | @property 24 | def illegal_builtin(self): 25 | return 'eval' 26 | -------------------------------------------------------------------------------- /dlint/linters/bad_pycrypto_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_module_use 11 | 12 | 13 | class BadPycryptoUseLinter(bad_module_use.BadModuleUseLinter): 14 | """This linter looks for use of the "Crypto" module. This module is part 15 | of the "pycrypto" library which is unmaintained and has known 16 | vulnerabilities and exploits. 17 | """ 18 | off_by_default = False 19 | 20 | _code = 'DUO133' 21 | _error_tmpl = 'DUO133 use of "Crypto" module is insecure' 22 | 23 | @property 24 | def illegal_modules(self): 25 | return [ 26 | "Crypto", 27 | ] 28 | -------------------------------------------------------------------------------- /docs/linters/DUO124.md: -------------------------------------------------------------------------------- 1 | # DUO124 2 | 3 | This linter looks for unsage usage of the `SimpleXMLRPCServer.register_instance` 4 | function. 5 | 6 | From the documentation: 7 | 8 | > Enabling the `allow_dotted_names` option allows intruders to access your 9 | > module's global variables and may allow intruders to execute arbitrary 10 | > code on your machine. Only use this option on a secure, closed network. 11 | 12 | ## Problematic code 13 | 14 | ```python 15 | import SimpleXMLRPCServer 16 | 17 | SimpleXMLRPCServer.register_instance(allow_dotted_names=True) 18 | ``` 19 | 20 | ## Correct code 21 | 22 | ```python 23 | import SimpleXMLRPCServer 24 | 25 | SimpleXMLRPCServer.register_instance() 26 | ``` 27 | 28 | ## Rationale 29 | 30 | See above. 31 | 32 | ## Exceptions 33 | 34 | * Code connecting to a secure, closed network 35 | -------------------------------------------------------------------------------- /dlint/linters/bad_hashlib_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_module_attribute_use 11 | 12 | 13 | class BadHashlibUseLinter(bad_module_attribute_use.BadModuleAttributeUseLinter): 14 | """This linter looks for unsafe use of the Python "hashlib" module. Use of 15 | md5|sha1 is known to have hash collision weaknesses. 16 | """ 17 | off_by_default = False 18 | 19 | _code = 'DUO130' 20 | _error_tmpl = 'DUO130 insecure use of "hashlib" module' 21 | 22 | @property 23 | def illegal_module_attributes(self): 24 | return { 25 | 'hashlib': [ 26 | 'md5', 27 | 'sha1', 28 | ], 29 | } 30 | -------------------------------------------------------------------------------- /dlint/linters/bad_dl_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_module_use 11 | 12 | 13 | class BadDlUseLinter(bad_module_use.BadModuleUseLinter): 14 | """This linter looks for use of the "dl" module. 15 | 16 | "The dl module bypasses the Python type system and error handling. If 17 | used incorrectly it may cause segmentation faults, crashes or other 18 | incorrect behaviour." 19 | 20 | https://docs.python.org/2.7/library/dl.html 21 | """ 22 | off_by_default = False 23 | 24 | _code = 'DUO117' 25 | _error_tmpl = 'DUO117 avoid "dl" module use' 26 | 27 | @property 28 | def illegal_modules(self): 29 | return [ 30 | "dl", 31 | ] 32 | -------------------------------------------------------------------------------- /dlint/linters/bad_gl_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_module_use 11 | 12 | 13 | class BadGlUseLinter(bad_module_use.BadModuleUseLinter): 14 | """This linter looks for use of the "gl" module. 15 | 16 | "Some illegal calls to the GL library cause the Python interpreter to 17 | dump core. In particular, the use of most GL calls is unsafe before the 18 | first window is opened." 19 | 20 | https://docs.python.org/2.7/library/gl.html 21 | """ 22 | off_by_default = False 23 | 24 | _code = 'DUO118' 25 | _error_tmpl = 'DUO118 avoid "gl" module use' 26 | 27 | @property 28 | def illegal_modules(self): 29 | return [ 30 | "gl", 31 | ] 32 | -------------------------------------------------------------------------------- /docs/linters/DUO112.md: -------------------------------------------------------------------------------- 1 | # DUO112 2 | 3 | This linter searches for use of the `extract` or `extractall` methods 4 | on `ZipFile` objects. Use of these functions allows for arbitrary file 5 | overwrites. 6 | 7 | ## Problematic code 8 | 9 | ```python 10 | import zipfile 11 | 12 | def func(): 13 | zf = zipfile.ZipFile() 14 | zf.extract() 15 | zf.extractall() 16 | ``` 17 | 18 | ## Correct code 19 | 20 | To ensure secure usage you must inspect the zipfile prior to extracting it 21 | 22 | ## Rationale 23 | 24 | From the Python documentation: 25 | 26 | > Never extract archives from untrusted sources without prior inspection. It 27 | > is possible that files are created outside of path, e.g. members that have 28 | > absolute filenames starting with `"/"` or filenames with two dots `".."`. 29 | 30 | ## Exceptions 31 | 32 | * Extracting zipfiles from trusted sources 33 | -------------------------------------------------------------------------------- /docs/linters/DUO115.md: -------------------------------------------------------------------------------- 1 | # DUO115 2 | 3 | This linter searches for use of the `extract` or `extractall` methods 4 | on `TarFile` objects. Use of these functions allows for arbitrary file 5 | overwrites. 6 | 7 | ## Problematic code 8 | 9 | ```python 10 | import tarfile 11 | 12 | def func(): 13 | tf = tarfile.TarFile() 14 | tf.extract() 15 | tf.extractall() 16 | ``` 17 | 18 | ## Correct code 19 | 20 | To ensure secure usage you must inspect the tarfile prior to extracting it 21 | 22 | ## Rationale 23 | 24 | From the Python documentation: 25 | 26 | > Never extract archives from untrusted sources without prior inspection. It 27 | > is possible that files are created outside of path, e.g. members that have 28 | > absolute filenames starting with `"/"` or filenames with two dots `".."`. 29 | 30 | ## Exceptions 31 | 32 | * Extracting tarfiles from trusted sources 33 | -------------------------------------------------------------------------------- /dlint/linters/bad_urllib3_module_attribute_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_module_attribute_use 11 | 12 | 13 | class BadUrllib3ModuleAttributeUseLinter(bad_module_attribute_use.BadModuleAttributeUseLinter): 14 | """This linter looks for unsafe use of urllib3 module attributes. These 15 | attributes may indicate insecure connections are being performed. 16 | """ 17 | off_by_default = False 18 | 19 | _code = 'DUO131' 20 | _error_tmpl = 'DUO131 "urllib3" warnings disabled, insecure connections possible' 21 | 22 | @property 23 | def illegal_module_attributes(self): 24 | return { 25 | 'urllib3': [ 26 | 'disable_warnings', 27 | ], 28 | } 29 | -------------------------------------------------------------------------------- /dlint/linters/bad_shelve_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_module_use 11 | 12 | 13 | class BadShelveUseLinter(bad_module_use.BadModuleUseLinter): 14 | """This linter looks for use of the "shelve" module. 15 | 16 | "Because the shelve module is backed by pickle, it is insecure to load 17 | a shelf from an untrusted source. Like with pickle, loading a shelf can 18 | execute arbitrary code." 19 | 20 | https://docs.python.org/2.7/library/shelve.html 21 | """ 22 | off_by_default = False 23 | 24 | _code = 'DUO119' 25 | _error_tmpl = 'DUO119 avoid "shelve" module use' 26 | 27 | @property 28 | def illegal_modules(self): 29 | return [ 30 | "shelve", 31 | ] 32 | -------------------------------------------------------------------------------- /dlint/linters/bad_marshal_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_module_use 11 | 12 | 13 | class BadMarshalUseLinter(bad_module_use.BadModuleUseLinter): 14 | """This linter looks for use of the "marshal" module. 15 | 16 | "The marshal module is not intended to be secure against erroneous 17 | or maliciously constructed data. Never unmarshal data received from an 18 | untrusted or unauthenticated source." 19 | 20 | https://docs.python.org/2.7/library/marshal.html 21 | """ 22 | off_by_default = False 23 | 24 | _code = 'DUO120' 25 | _error_tmpl = 'DUO120 avoid "marshal" module use' 26 | 27 | @property 28 | def illegal_modules(self): 29 | return [ 30 | "marshal", 31 | ] 32 | -------------------------------------------------------------------------------- /dlint/linters/bad_zipfile_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_name_attribute_use 11 | 12 | 13 | class BadZipfileUseLinter(bad_name_attribute_use.BadNameAttributeUseLinter): 14 | """This linter looks for use of the Python "zipfile" library 15 | "extract|extractall" function. These functions allows for arbitrary file 16 | overwrite. 17 | """ 18 | off_by_default = False 19 | 20 | _code = 'DUO112' 21 | _error_tmpl = 'DUO112 use of "extract|extractall" is insecure' 22 | 23 | @property 24 | def illegal_name_attributes(self): 25 | return { 26 | "extract": [ 27 | "zipfile.ZipFile", 28 | ], 29 | "extractall": [ 30 | "zipfile.ZipFile", 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /dlint/linters/bad_xml_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_module_use 11 | 12 | 13 | class BadXMLUseLinter(bad_module_use.BadModuleUseLinter): 14 | """This linter looks for use of the Python "xml" module EXCEPT defusedxml. 15 | Etree and Minidom are bad - using TreeBuilder is okay because it does not 16 | parse xml. 17 | """ 18 | off_by_default = False 19 | 20 | _code = 'DUO107' 21 | _error_tmpl = 'DUO107 insecure use of XML modules, prefer "defusedxml"' 22 | 23 | @property 24 | def illegal_modules(self): 25 | return [ 26 | 'lxml', 27 | 'xml', 28 | 'xmlrpclib', 29 | ] 30 | 31 | @property 32 | def whitelisted_modules(self): 33 | return [ 34 | 'xml.sax.saxutils', 35 | ] 36 | -------------------------------------------------------------------------------- /docs/linters/DUO114.md: -------------------------------------------------------------------------------- 1 | # DUO114 2 | 3 | This linter looks for `returnValue` calls that are in a function missing a 4 | `inlineCallbacks` decorator. 5 | 6 | ## Problematic code 7 | 8 | ```python 9 | from twisted.internet import defer 10 | 11 | def func(arg): 12 | result = yield other_inlinecallbacks_func(arg + 5) 13 | defer.returnValue(result) 14 | ``` 15 | 16 | ## Correct code 17 | 18 | ```python 19 | from twisted.internet import defer 20 | 21 | @defer.inlineCallbacks 22 | def func(arg): 23 | result = yield other_inlinecallbacks_func(arg + 5) 24 | defer.returnValue(result) 25 | ``` 26 | 27 | ## Rationale 28 | 29 | A `returnValue` call implies that a function should be using `inlineCallbacks`. 30 | For more information see [returnValue](https://twistedmatrix.com/documents/current/api/twisted.internet.defer.html#returnValue). 31 | 32 | ## Exceptions 33 | 34 | None - if you don't need `inlineCallbacks` you can simply use `return` 35 | -------------------------------------------------------------------------------- /tests/test_bad_dl_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadDlUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_dl_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import dl 21 | """ 22 | ) 23 | 24 | linter = dlint.linters.BadDlUseLinter() 25 | linter.visit(python_node) 26 | 27 | result = linter.get_results() 28 | expected = [ 29 | dlint.linters.base.Flake8Result( 30 | lineno=2, 31 | col_offset=0, 32 | message=dlint.linters.BadDlUseLinter._error_tmpl 33 | ) 34 | ] 35 | 36 | assert result == expected 37 | 38 | 39 | if __name__ == "__main__": 40 | unittest.main() 41 | -------------------------------------------------------------------------------- /tests/test_bad_gl_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadGlUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_gl_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import gl 21 | """ 22 | ) 23 | 24 | linter = dlint.linters.BadGlUseLinter() 25 | linter.visit(python_node) 26 | 27 | result = linter.get_results() 28 | expected = [ 29 | dlint.linters.base.Flake8Result( 30 | lineno=2, 31 | col_offset=0, 32 | message=dlint.linters.BadGlUseLinter._error_tmpl 33 | ) 34 | ] 35 | 36 | assert result == expected 37 | 38 | 39 | if __name__ == "__main__": 40 | unittest.main() 41 | -------------------------------------------------------------------------------- /dlint/linters/bad_yaml_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_module_attribute_use 11 | 12 | 13 | class BadYAMLUseLinter(bad_module_attribute_use.BadModuleAttributeUseLinter): 14 | """This linter looks for unsafe use of the Python "yaml" module. Its 15 | parsing functions (dump, dump_all, load, load_all) should be avoided in 16 | favor of their safe_* equivalent. 17 | """ 18 | off_by_default = False 19 | 20 | _code = 'DUO109' 21 | _error_tmpl = 'DUO109 insecure use of "yaml" parsing function, prefer "safe_*" equivalent' 22 | 23 | @property 24 | def illegal_module_attributes(self): 25 | return { 26 | 'yaml': [ 27 | 'dump', 28 | 'dump_all', 29 | 'load', 30 | 'load_all', 31 | ], 32 | } 33 | -------------------------------------------------------------------------------- /dlint/linters/bad_sys_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_module_attribute_use 11 | 12 | 13 | class BadSysUseLinter(bad_module_attribute_use.BadModuleAttributeUseLinter): 14 | """This linter looks for unsafe use of the Python "sys" module. These 15 | functions allow for an arbitrary function to be passed in that the 16 | interpreter will call at a later point in time. This could lead to 17 | arbitrary code execution. 18 | """ 19 | off_by_default = False 20 | 21 | _code = 'DUO111' 22 | _error_tmpl = 'DUO111 insecure use of "sys" module' 23 | 24 | @property 25 | def illegal_module_attributes(self): 26 | return { 27 | 'sys': [ 28 | 'call_tracing', 29 | 'setprofile', 30 | 'settrace', 31 | ], 32 | } 33 | -------------------------------------------------------------------------------- /tests/test_bad_popen2_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadPopen2Use(dlint.test.base.BaseTest): 16 | 17 | def test_bad_popen2_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import popen2 21 | """ 22 | ) 23 | 24 | linter = dlint.linters.BadPopen2UseLinter() 25 | linter.visit(python_node) 26 | 27 | result = linter.get_results() 28 | expected = [ 29 | dlint.linters.base.Flake8Result( 30 | lineno=2, 31 | col_offset=0, 32 | message=dlint.linters.BadPopen2UseLinter._error_tmpl 33 | ) 34 | ] 35 | 36 | assert result == expected 37 | 38 | 39 | if __name__ == "__main__": 40 | unittest.main() 41 | -------------------------------------------------------------------------------- /tests/test_bad_shelve_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadShelveUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_shelve_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import shelve 21 | """ 22 | ) 23 | 24 | linter = dlint.linters.BadShelveUseLinter() 25 | linter.visit(python_node) 26 | 27 | result = linter.get_results() 28 | expected = [ 29 | dlint.linters.base.Flake8Result( 30 | lineno=2, 31 | col_offset=0, 32 | message=dlint.linters.BadShelveUseLinter._error_tmpl 33 | ) 34 | ] 35 | 36 | assert result == expected 37 | 38 | 39 | if __name__ == "__main__": 40 | unittest.main() 41 | -------------------------------------------------------------------------------- /tests/test_bad_marshal_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadMarshalUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_marshal_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import marshal 21 | """ 22 | ) 23 | 24 | linter = dlint.linters.BadMarshalUseLinter() 25 | linter.visit(python_node) 26 | 27 | result = linter.get_results() 28 | expected = [ 29 | dlint.linters.base.Flake8Result( 30 | lineno=2, 31 | col_offset=0, 32 | message=dlint.linters.BadMarshalUseLinter._error_tmpl 33 | ) 34 | ] 35 | 36 | assert result == expected 37 | 38 | 39 | if __name__ == "__main__": 40 | unittest.main() 41 | -------------------------------------------------------------------------------- /tests/test_bad_commands_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadCommandsUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_commands_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import commands 21 | """ 22 | ) 23 | 24 | linter = dlint.linters.BadCommandsUseLinter() 25 | linter.visit(python_node) 26 | 27 | result = linter.get_results() 28 | expected = [ 29 | dlint.linters.base.Flake8Result( 30 | lineno=2, 31 | col_offset=0, 32 | message=dlint.linters.BadCommandsUseLinter._error_tmpl 33 | ) 34 | ] 35 | 36 | assert result == expected 37 | 38 | 39 | if __name__ == "__main__": 40 | unittest.main() 41 | -------------------------------------------------------------------------------- /dlint/linters/bad_onelogin_module_attribute_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_module_attribute_use 11 | 12 | 13 | class BadOneLoginModuleAttributeUseLinter(bad_module_attribute_use.BadModuleAttributeUseLinter): 14 | """This linter looks for unsafe use of OneLogin SAML module attributes. 15 | These attributes may indicate weaknesses in SAML authentication support. 16 | """ 17 | off_by_default = False 18 | 19 | _code = 'DUO129' 20 | _error_tmpl = 'DUO129 insecure "OneLogin" SAML attribute use' 21 | 22 | @property 23 | def illegal_module_attributes(self): 24 | return { 25 | 'onelogin.saml2.utils.OneLogin_Saml2_Constants': [ 26 | 'SHA1', 27 | 'RSA_SHA1', 28 | 'DSA_SHA1', 29 | 'TRIPLEDES_CBC', 30 | ], 31 | } 32 | -------------------------------------------------------------------------------- /tests/test_bad_eval_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadEvalUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_eval_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | var = 1 21 | 22 | result = eval('var + 1') 23 | """ 24 | ) 25 | 26 | linter = dlint.linters.BadEvalUseLinter() 27 | linter.visit(python_node) 28 | 29 | result = linter.get_results() 30 | expected = [ 31 | dlint.linters.base.Flake8Result( 32 | lineno=4, 33 | col_offset=9, 34 | message=dlint.linters.BadEvalUseLinter._error_tmpl 35 | ) 36 | ] 37 | 38 | assert result == expected 39 | 40 | 41 | if __name__ == "__main__": 42 | unittest.main() 43 | -------------------------------------------------------------------------------- /dlint/linters/bad_pickle_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_module_attribute_use 11 | 12 | 13 | class BadPickleUseLinter(bad_module_attribute_use.BadModuleAttributeUseLinter): 14 | """This linter looks for use of the Python "pickle" module. Pickling is 15 | not secure against erroneous or maliciously constructed data. 16 | """ 17 | off_by_default = False 18 | 19 | _code = 'DUO103' 20 | _error_tmpl = 'DUO103 insecure use of "pickle" or "cPickle"' 21 | 22 | @property 23 | def illegal_module_attributes(self): 24 | return { 25 | 'cPickle': [ 26 | 'loads', 27 | 'load', 28 | 'Unpickler', 29 | ], 30 | 'pickle': [ 31 | 'loads', 32 | 'load', 33 | 'Unpickler', 34 | ], 35 | } 36 | -------------------------------------------------------------------------------- /dlint/linters/bad_tarfile_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_name_attribute_use 11 | 12 | 13 | class BadTarfileUseLinter(bad_name_attribute_use.BadNameAttributeUseLinter): 14 | """This linter looks for use of the Python "tarfile" library 15 | "extract|extractall" function. These functions allows for arbitrary file 16 | overwrite. 17 | """ 18 | off_by_default = False 19 | 20 | _code = 'DUO115' 21 | _error_tmpl = 'DUO115 use of "extract|extractall" is insecure' 22 | 23 | @property 24 | def illegal_name_attributes(self): 25 | return { 26 | "extract": [ 27 | "tarfile.TarFile.open", 28 | "tarfile.TarFile", 29 | ], 30 | "extractall": [ 31 | "tarfile.TarFile.open", 32 | "tarfile.TarFile", 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /docs/linters/DUO131.md: -------------------------------------------------------------------------------- 1 | # DUO131 2 | 3 | This linter searches for insecure attribute use of the `urllib3` module. 4 | Specifically, using the `disable_warnings` function. This function disables 5 | warnings such as unverified HTTPS requests, HTTPS request without SNI 6 | available, HTTPS request to a host with a certificate missing a SAN, etc. 7 | 8 | ## Problematic code 9 | 10 | ```python 11 | import urllib3 12 | urllib3.disable_warnings() 13 | ``` 14 | 15 | ## Correct code 16 | 17 | There is no correct usage of this function 18 | 19 | ## Rationale 20 | 21 | Warnings produced by `urllib3` are intended to prevent the user from 22 | performing insecure web requests (along with preventing other common, 23 | non-security-related errors). Disabling this functionality allows for insecure 24 | requests to go unnoticed. 25 | 26 | ## Exceptions 27 | 28 | * Code connecting to internal network services (although these should strive for full HTTPS as well) 29 | * Code connecting to local development services or in test environments 30 | -------------------------------------------------------------------------------- /docs/linters/DUO101.md: -------------------------------------------------------------------------------- 1 | # DUO101 2 | 3 | **Note: This rule only applies to Python < 3.3** 4 | 5 | This linter looks for `inlineCallbacks` functions that have non-empty 6 | `return` statements. Using a non-empty `return` statement and a `yield` 7 | statement in the same function is a syntax error. 8 | 9 | This is **not a security bug**, but is a common bug when building `twisted` 10 | applications. 11 | 12 | ## Problematic code 13 | 14 | ```python 15 | from twisted.internet import defer 16 | 17 | @defer.inlineCallbacks 18 | def func(arg): 19 | return arg 20 | ``` 21 | 22 | ## Correct code 23 | 24 | ```python 25 | from twisted.internet import defer 26 | 27 | @defer.inlineCallbacks 28 | def func(arg): 29 | defer.returnValue(arg) 30 | ``` 31 | 32 | ## Rationale 33 | 34 | For more information see [Introduction to Deferreds](https://twisted.readthedocs.io/en/latest/core/howto/defer-intro.html). 35 | 36 | ## Exceptions 37 | 38 | * If you are using Python 3.3+, it is possible to use the `return` statement instead of `returnValue` 39 | -------------------------------------------------------------------------------- /tests/test_bad_tempfile_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadTempfileUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_tempfile_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import tempfile 21 | 22 | tempfile.mktemp() 23 | """ 24 | ) 25 | 26 | linter = dlint.linters.BadTempfileUseLinter() 27 | linter.visit(python_node) 28 | 29 | result = linter.get_results() 30 | expected = [ 31 | dlint.linters.base.Flake8Result( 32 | lineno=4, 33 | col_offset=0, 34 | message=dlint.linters.BadTempfileUseLinter._error_tmpl 35 | ), 36 | ] 37 | 38 | assert result == expected 39 | 40 | 41 | if __name__ == "__main__": 42 | unittest.main() 43 | -------------------------------------------------------------------------------- /docs/linters/DUO107.md: -------------------------------------------------------------------------------- 1 | # DUO107 2 | 3 | This linter searches for use of the `lxml`, `xml`, `xmlrpclib` modules. These 4 | libraries are not hardened against many common XML attacks. The `defusedxml` 5 | library should be preferred to these modules. 6 | 7 | ## Problematic code 8 | 9 | ```python 10 | from xml.etree.ElementTree import parse 11 | et = parse(xmlfile) 12 | ``` 13 | 14 | ## Correct code 15 | 16 | ```python 17 | from defusedxml.ElementTree import parse 18 | et = parse(xmlfile) 19 | ``` 20 | 21 | ## Rationale 22 | 23 | The XML data format is notoriously complex and provides many attack vectors. 24 | Common Python XML libriaries are vulnerable to various attacks such as 25 | exponential entity expansion, quadratic entity expansion, remote and local 26 | external entity expansion, DTD retrieval, etc. 27 | 28 | For more information on specific vulnerabilities see [Python XML Libraries](https://pypi.org/project/defusedxml/#python-xml-libraries). 29 | 30 | ## Exceptions 31 | 32 | * The `xml.sax.saxutils` sub-module is safe to use 33 | -------------------------------------------------------------------------------- /dlint/linters/bad_exec_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_builtin_use 11 | from . import base 12 | 13 | 14 | class BadExecUseLinter(bad_builtin_use.BadBuiltinUseLinter): 15 | """This linter looks for use of the Python "exec" function. This function 16 | makes it far too easy to achieve arbitrary code execution, so we shouldn't 17 | support it in any context. 18 | """ 19 | off_by_default = False 20 | 21 | _code = 'DUO105' 22 | _error_tmpl = 'DUO105 use of "exec" is insecure' 23 | 24 | # Python 2 25 | def visit_Exec(self, node): 26 | self.results.append( 27 | base.Flake8Result( 28 | lineno=node.lineno, 29 | col_offset=node.col_offset, 30 | message=self._error_tmpl 31 | ) 32 | ) 33 | 34 | @property 35 | def illegal_builtin(self): 36 | return 'exec' 37 | -------------------------------------------------------------------------------- /tests/test_bad_compile_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadCompileUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_compile_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | var = 1 21 | 22 | result = compile('var + 1', '', 'eval') 23 | """ 24 | ) 25 | 26 | linter = dlint.linters.BadCompileUseLinter() 27 | linter.visit(python_node) 28 | 29 | result = linter.get_results() 30 | expected = [ 31 | dlint.linters.base.Flake8Result( 32 | lineno=4, 33 | col_offset=9, 34 | message=dlint.linters.BadCompileUseLinter._error_tmpl 35 | ) 36 | ] 37 | 38 | assert result == expected 39 | 40 | 41 | if __name__ == "__main__": 42 | unittest.main() 43 | -------------------------------------------------------------------------------- /tests/test_bad_xmlrpc_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadXmlrpcUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_xmlrpc_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import SimpleXMLRPCServer 21 | 22 | SimpleXMLRPCServer.register_instance(allow_dotted_names=True) 23 | """ 24 | ) 25 | 26 | linter = dlint.linters.BadXmlrpcUseLinter() 27 | linter.visit(python_node) 28 | 29 | result = linter.get_results() 30 | expected = [ 31 | dlint.linters.base.Flake8Result( 32 | lineno=4, 33 | col_offset=0, 34 | message=dlint.linters.BadXmlrpcUseLinter._error_tmpl 35 | ), 36 | ] 37 | 38 | assert result == expected 39 | 40 | 41 | if __name__ == "__main__": 42 | unittest.main() 43 | -------------------------------------------------------------------------------- /dlint/linters/bad_compile_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_builtin_use 11 | 12 | 13 | class BadCompileUseLinter(bad_builtin_use.BadBuiltinUseLinter): 14 | """This linter looks for use of the Python "compile" function. While not 15 | bad in and of itself, this function is _probably_ a code smell that 16 | something else we don't want could be going on. I.e. "compile" is often 17 | proceeded by "eval" or "exec". 18 | 19 | The Python docs also have this to say about "compile": 20 | 21 | "Warning: It is possible to crash the Python interpreter with a 22 | sufficiently large/complex string when compiling to an AST object 23 | due to stack depth limitations in Python's AST compiler." 24 | """ 25 | off_by_default = False 26 | 27 | _code = 'DUO110' 28 | _error_tmpl = 'DUO110 use of "compile" is insecure' 29 | 30 | @property 31 | def illegal_builtin(self): 32 | return 'compile' 33 | -------------------------------------------------------------------------------- /docs/linters/DUO102.md: -------------------------------------------------------------------------------- 1 | # DUO102 2 | 3 | This linter searches for `random` module use not using `random.SystemRandom`. 4 | The `random` module is not suitable for security or cryptographic uses. 5 | 6 | ## Problematic code 7 | 8 | ```python 9 | import random 10 | 11 | browser_cookie = random.randint(min_value, max_value) 12 | ``` 13 | 14 | ## Correct code 15 | 16 | ```python 17 | from random import SystemRandom 18 | safe_random = SystemRandom() 19 | 20 | browser_cookie = safe_random.randint(min_value, max_value) 21 | ``` 22 | 23 | When using Python 3.6+ it is also advisable to use the `secrets` module. 24 | 25 | ## Rationale 26 | 27 | Python uses the Mersenne Twister as the core generator. However, being 28 | completely deterministic, it is not suitable for all purposes, and is 29 | completely unsuitable for cryptographic purposes. Because the generator is 30 | deterministic this means attackers can predict future values given a 31 | sufficient amount of previous values. 32 | 33 | ## Exceptions 34 | 35 | * Normal `random` use is acceptable if the relevant code is not used for security or cryptographic purposes 36 | -------------------------------------------------------------------------------- /docs/linters/DUO104.md: -------------------------------------------------------------------------------- 1 | # DUO104 2 | 3 | This linter searches for use of the built-in `eval` function. This function 4 | commonly allows for arbitrary code execution bugs when combined with user 5 | input. 6 | 7 | ## Problematic code 8 | 9 | ```python 10 | malicious_user_input = 'open("/etc/passwd")' 11 | eval(malicious_user_input) 12 | ``` 13 | 14 | ## Correct code 15 | 16 | ```python 17 | # Constant argument 18 | eval('2 + 2') 19 | ``` 20 | 21 | ## Rationale 22 | 23 | Arbitrary code execution allows an attacker to perform any action within the 24 | context of the system the bug is found. E.g. open a reverse shell to a system 25 | of their choosing, install malware by downloading and running a payload, 26 | silently log actions performed on the victim system, etc. 27 | 28 | Arbitrary code execution bugs are effectively the keys to the castle. We'd 29 | like to avoid using the above function because it commonly allows for these 30 | types of bugs. 31 | 32 | ## Exceptions 33 | 34 | * Code may be safe if data passed to `eval` contains no user input 35 | * Code may be safe if data passed to `eval` is a constant string 36 | -------------------------------------------------------------------------------- /tests/test_benchmark/conftest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import argparse 11 | import ast 12 | 13 | import pytest 14 | 15 | 16 | def pytest_addoption(parser): 17 | parser.addoption( 18 | "--benchmark-py-file", 19 | action="store", 20 | type=argparse.FileType("r"), 21 | help="Benchmark Dlint against this Python file." 22 | ) 23 | parser.addoption( 24 | "--benchmark-group-base-class", 25 | action="store_true", 26 | help="Group Dlint benchmark results by base class." 27 | ) 28 | 29 | 30 | @pytest.fixture 31 | def benchmark_py_file(request): 32 | fd = request.config.getoption("--benchmark-py-file", skip=True) 33 | 34 | if fd.tell() > 0: 35 | # Read calls from previous tests exhaust the file descriptor 36 | fd.seek(0) 37 | 38 | return ast.parse(fd.read()) 39 | 40 | 41 | @pytest.fixture 42 | def benchmark_group_base_class(request): 43 | return request.config.getoption("--benchmark-group-base-class") 44 | -------------------------------------------------------------------------------- /dlint/linters/helpers/bad_builtin_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import abc 11 | 12 | from .. import base 13 | from ... import util 14 | 15 | 16 | class BadBuiltinUseLinter(base.BaseLinter, util.ABC): 17 | """This abstract base class provides an simple interface for creating new 18 | lint rules that block builtin functions. 19 | """ 20 | @property 21 | @abc.abstractmethod 22 | def illegal_builtin(self): 23 | """Subclasses must implement this property to return a string of the 24 | builtin function name they'd like to blacklist. 25 | """ 26 | 27 | def visit_Name(self, node): 28 | if (node.id == self.illegal_builtin 29 | and not self.namespace.name_imported(node.id)): 30 | self.results.append( 31 | base.Flake8Result( 32 | lineno=node.lineno, 33 | col_offset=node.col_offset, 34 | message=self._error_tmpl 35 | ) 36 | ) 37 | -------------------------------------------------------------------------------- /docs/linters/DUO113.md: -------------------------------------------------------------------------------- 1 | # DUO113 2 | 3 | This linter looks for `inlineCallbacks` functions that are missing a `yield` 4 | statement. The presence of a yield statement turns a normal function into a 5 | generator and the inlineCallback generator depends on this behavior. 6 | 7 | This is **not a security bug**, but is a common bug when building `twisted` 8 | applications. 9 | 10 | ## Problematic code 11 | 12 | ```python 13 | from twisted.internet import defer 14 | 15 | @defer.inlineCallbacks 16 | def func(arg): 17 | result = other_inlinecallbacks_func(arg + 5) 18 | defer.returnValue(result) 19 | ``` 20 | 21 | ## Correct code 22 | 23 | ```python 24 | from twisted.internet import defer 25 | 26 | @defer.inlineCallbacks 27 | def func(arg): 28 | result = yield other_inlinecallbacks_func(arg + 5) 29 | defer.returnValue(result) 30 | ``` 31 | 32 | ## Rationale 33 | 34 | For more information see [Inline callbacks - using 'yield'](https://twistedmatrix.com/documents/current/core/howto/defer-intro.html#inline-callbacks-using-yield). 35 | 36 | ## Exceptions 37 | 38 | None - if you don't need a `yield` you should not be using `inlineCallbacks` 39 | -------------------------------------------------------------------------------- /docs/linters/DUO126.md: -------------------------------------------------------------------------------- 1 | # DUO126 2 | 3 | This linter searches for use of the `popen2` module. 4 | 5 | Use of the `popen2` module commonly allows for arbitrary code execution 6 | bugs when combined with user input. 7 | 8 | ## Problematic code 9 | 10 | ```python 11 | import popen2 12 | ``` 13 | 14 | ## Correct code 15 | 16 | Instead of using the `popen2` module to execute commands, prefer to use the 17 | `subprocess` module while ensuring `shell=False`. 18 | 19 | See [Replacing Older Functions with the `subprocess` Module](https://docs.python.org/3/library/subprocess.html#subprocess-replacements). 20 | 21 | ## Rationale 22 | 23 | Arbitrary code execution allows an attacker to perform any action within the 24 | context of the system the bug is found. E.g. open a reverse shell to a system 25 | of their choosing, install malware by downloading and running a payload, 26 | silently log actions performed on the victim system, etc. 27 | 28 | Arbitrary code execution bugs are effectively the keys to the castle. We'd 29 | like to avoid using the above function because it commonly allows for these 30 | types of bugs. 31 | 32 | ## Exceptions 33 | 34 | None 35 | -------------------------------------------------------------------------------- /docs/linters/DUO123.md: -------------------------------------------------------------------------------- 1 | # DUO123 2 | 3 | This linter looks for unsafe use of the Python `requests` library. 4 | 5 | This library has become the de facto standard for making HTTP requests. This 6 | linter specifically searches for situations where HTTPS requests are made with 7 | SSL certificate verification turned off via the `verify=False` keyword argument. 8 | 9 | ## Problematic code 10 | 11 | ```python 12 | import requests 13 | 14 | request = requests.get("https://google.com", verify=False) 15 | ``` 16 | 17 | ## Correct code 18 | 19 | ```python 20 | import requests 21 | 22 | request = requests.get("https://google.com") 23 | ``` 24 | 25 | ## Rationale 26 | 27 | HTTPS certificate verification ensures your requests are communicating with 28 | whom they're supposed to be. Without this there could be an attacker 29 | impersonating the server you intend to be communicating with. To prevent this 30 | you must ensure certification verification is enabled. 31 | 32 | ## Exceptions 33 | 34 | * Code connecting to internal network services (although these should strive for full HTTPS as well) 35 | * Code connecting to local development services or in test environments 36 | -------------------------------------------------------------------------------- /docs/linters/DUO127.md: -------------------------------------------------------------------------------- 1 | # DUO127 2 | 3 | This linter searches for insecure use of the `duo_client` module. 4 | 5 | More specifically, it searches for making insecure HTTP requests or 6 | HTTPS requests with certificate verification disabled when communicating 7 | with the Duo API. 8 | 9 | ## Problematic code 10 | 11 | ```python 12 | import duo_client 13 | 14 | client = duo_client.Client(ikey=..., skey=..., host=..., ca_certs="HTTP") 15 | ``` 16 | 17 | ```python 18 | import duo_client 19 | 20 | client = duo_client.Client(ikey=..., skey=..., host=..., ca_certs="DISABLE") 21 | ``` 22 | 23 | ## Correct code 24 | 25 | ```python 26 | import duo_client 27 | 28 | client = duo_client.Client(ikey=..., skey=..., host=...) 29 | ``` 30 | 31 | ## Rationale 32 | 33 | HTTPS with certificate verification enabled ensures your requests are 34 | communicating with whom they're supposed to be. Without this there could be an 35 | attacker impersonating the server you intend to be communicating with. To 36 | prevent this you must ensure certification verification is enabled. 37 | 38 | ## Exceptions 39 | 40 | * Code connecting to local development services or in test environments 41 | -------------------------------------------------------------------------------- /dlint/linters/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import ast 11 | import collections 12 | 13 | from .. import namespace 14 | 15 | Flake8Result = collections.namedtuple( 16 | 'Flake8Result', 17 | ['lineno', 'col_offset', 'message'] 18 | ) 19 | 20 | 21 | class BaseLinter(ast.NodeVisitor): 22 | def __init__(self, *args, **kwargs): 23 | self.results = [] 24 | self.namespace = None 25 | 26 | super(BaseLinter, self).__init__(*args, **kwargs) 27 | 28 | def get_results(self): 29 | 30 | return self.results 31 | 32 | def visit(self, node): 33 | if not self.namespace: 34 | # In MultiNodeVisitor runs this will have already been set since 35 | # the namespace remains the same for each linter. However, during 36 | # testing or single-linter runs we still need to initialize the 37 | # namespace for the linter. 38 | self.namespace = namespace.Namespace.from_module_node(node) 39 | 40 | super(BaseLinter, self).visit(node) 41 | -------------------------------------------------------------------------------- /docs/linters/DUO108.md: -------------------------------------------------------------------------------- 1 | # DUO108 2 | 3 | **Note: This rule only applies to Python 2** 4 | 5 | This linter searches for use of the built-in `input` function. This function 6 | is equivalent to `eval(raw_input(...))`, and thus commonly allows for arbitrary 7 | code execution bugs when combined with user input. 8 | 9 | ## Problematic code 10 | 11 | ```python 12 | input('Text here: ') 13 | Text here: open('/etc/passwd').read() 14 | ``` 15 | 16 | ## Correct code 17 | 18 | ```python 19 | raw_input('Text here: ') 20 | Text here: open('/etc/passwd').read() 21 | ``` 22 | 23 | ## Rationale 24 | 25 | Arbitrary code execution allows an attacker to perform any action within the 26 | context of the system the bug is found. E.g. open a reverse shell to a system 27 | of their choosing, install malware by downloading and running a payload, 28 | silently log actions performed on the victim system, etc. 29 | 30 | Arbitrary code execution bugs are effectively the keys to the castle. We'd 31 | like to avoid using the above function because it commonly allows for these 32 | types of bugs. 33 | 34 | ## Exceptions 35 | 36 | * Code using `six.moves.input` behaves like Python 3 `input`, and thus is safe 37 | -------------------------------------------------------------------------------- /dlint/linters/bad_tempfile_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_module_attribute_use 11 | 12 | 13 | class BadTempfileUseLinter(bad_module_attribute_use.BadModuleAttributeUseLinter): 14 | """This linter looks for unsafe use of the Python "tempfile.mktemp" module: 15 | 16 | "Use of this function may introduce a security hole in your program. By 17 | the time you get around to doing anything with the file name it 18 | returns, someone else may have beaten you to the punch. mktemp() usage 19 | can be replaced easily with NamedTemporaryFile(), passing it the 20 | delete=False parameter." 21 | 22 | https://docs.python.org/2.7/library/tempfile.html 23 | """ 24 | off_by_default = False 25 | 26 | _code = 'DUO121' 27 | _error_tmpl = 'DUO121 use of "tempfile.mktemp" allows for race conditions' 28 | 29 | @property 30 | def illegal_module_attributes(self): 31 | return { 32 | 'tempfile': [ 33 | 'mktemp', 34 | ], 35 | } 36 | -------------------------------------------------------------------------------- /docs/linters/DUO130.md: -------------------------------------------------------------------------------- 1 | # DUO130 2 | 3 | This linter searches for insecure use of the `hashlib` module. 4 | 5 | More specifically this searches for MD5 or SHA1 use when hashing. Both of these 6 | hashing algorithms are considered insecure at this point: 7 | 8 | * [Is MD5 considered insecure?](https://security.stackexchange.com/questions/19906/is-md5-considered-insecure) 9 | * [How secure is SHA1? What are the chances of a real exploit?](https://crypto.stackexchange.com/questions/48289/how-secure-is-sha1-what-are-the-chances-of-a-real-exploit) 10 | 11 | ## Problematic code 12 | 13 | ```python 14 | import hashlib 15 | 16 | md5_hashed = hashlib.md5(b"data").hexdigest() 17 | sha1_hashed = hashlib.sha1(b"data").hexdigest() 18 | ``` 19 | 20 | ## Correct code 21 | 22 | ```python 23 | import hashlib 24 | 25 | sha256_hashed = hashlib.sha256(b"data").hexdigest() 26 | sha512_hashed = hashlib.sha512(b"data").hexdigest() 27 | blake2b_hashed = hashlib.blake2b(b"data").hexdigest() 28 | # ... 29 | ``` 30 | 31 | ## Rationale 32 | 33 | Some algorithms have known hash collision weaknesses. 34 | 35 | ## Exceptions 36 | 37 | * Compatibility with systems that can only use MD5 or SHA1 and are not under your control 38 | -------------------------------------------------------------------------------- /dlint/linters/bad_os_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_module_attribute_use 11 | 12 | 13 | class BadOSUseLinter(bad_module_attribute_use.BadModuleAttributeUseLinter): 14 | """This linter looks for unsafe use of the Python "os" module. Use of 15 | system|popen|popen2|popen3|popen4 allows for easy code execution bugs. 16 | Further: 17 | 18 | "Use of tempnam|tmpnam() is vulnerable to symlink attacks; consider 19 | using tmpfile() (section File Object Creation) instead." 20 | 21 | https://docs.python.org/2.7/library/os.html 22 | """ 23 | off_by_default = False 24 | 25 | _code = 'DUO106' 26 | _error_tmpl = 'DUO106 insecure use of "os" module' 27 | 28 | @property 29 | def illegal_module_attributes(self): 30 | return { 31 | 'os': [ 32 | 'popen', 33 | 'popen2', 34 | 'popen3', 35 | 'popen4', 36 | 'system', 37 | 'tempnam', 38 | 'tmpnam', 39 | ], 40 | } 41 | -------------------------------------------------------------------------------- /docs/linters/DUO125.md: -------------------------------------------------------------------------------- 1 | # DUO125 2 | 3 | This linter searches for use of the `commands` module. 4 | 5 | Use of the `commands` module commonly allows for arbitrary code execution 6 | bugs when combined with user input. 7 | 8 | ## Problematic code 9 | 10 | ```python 11 | import commands 12 | 13 | commands.getstatusoutput('ls /bin/ls') 14 | ``` 15 | 16 | ## Correct code 17 | 18 | Instead of using the `commands` module to execute commands, prefer to use the 19 | `subprocess` module while ensuring `shell=False`. 20 | 21 | See [Replacing Older Functions with the `subprocess` Module](https://docs.python.org/3/library/subprocess.html#subprocess-replacements). 22 | 23 | ## Rationale 24 | 25 | Arbitrary code execution allows an attacker to perform any action within the 26 | context of the system the bug is found. E.g. open a reverse shell to a system 27 | of their choosing, install malware by downloading and running a payload, 28 | silently log actions performed on the victim system, etc. 29 | 30 | Arbitrary code execution bugs are effectively the keys to the castle. We'd 31 | like to avoid using the above function because it commonly allows for these 32 | types of bugs. 33 | 34 | ## Exceptions 35 | 36 | None 37 | -------------------------------------------------------------------------------- /docs/linters/DUO111.md: -------------------------------------------------------------------------------- 1 | # DUO111 2 | 3 | This linter searches for insecure use of the `sys` module. 4 | 5 | Use of `call_tracing`, `setprofile`, or `settrace` can allow for 6 | arbitrary code execution bugs. 7 | 8 | ## Problematic code 9 | 10 | ```python 11 | import sys 12 | 13 | sys.call_tracing(func, args) 14 | sys.setprofile(func, args) 15 | sys.settrace(func, args) 16 | ``` 17 | 18 | ## Correct code 19 | 20 | This functionality can correctly be used to debug code, but rarely, if ever, 21 | should be used in production code. 22 | 23 | ## Rationale 24 | 25 | Debuggers or code profilers will commonly use these functions, however, 26 | attackers may use these functions to enable advanced persistent threats (APTs). 27 | This can be achieved by smuggling code onto a victim's machine then 28 | persistently calling it via the above functions. 29 | 30 | For more information see [PEP 551](https://www.python.org/dev/peps/pep-0551/) 31 | and [PEP 578](https://www.python.org/dev/peps/pep-0578/). More specifically, 32 | PEP 578 calls out these functions as "Suggested Audit Hooks." 33 | 34 | ## Exceptions 35 | 36 | * Debugging code in a development environment 37 | * Code that is explicitly used as a debugger, profiler, etc 38 | -------------------------------------------------------------------------------- /dlint/linters/bad_xmlsec_module_attribute_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_module_attribute_use 11 | 12 | 13 | class BadXmlsecModuleAttributeUseLinter(bad_module_attribute_use.BadModuleAttributeUseLinter): 14 | """This linter looks for unsafe use of xmlsec module attributes. 15 | These attributes may indicate weaknesses in cryptographic operations. 16 | """ 17 | off_by_default = False 18 | 19 | _code = 'DUO136' 20 | _error_tmpl = 'DUO136 insecure "xmlsec" attribute use' 21 | 22 | @property 23 | def illegal_module_attributes(self): 24 | return { 25 | 'xmlsec.constants': [ 26 | 'TransformDes3Cbc', 27 | 'TransformKWDes3', 28 | 'TransformDsaSha1', 29 | 'TransformEcdsaSha1', 30 | 'TransformRsaMd5', 31 | 'TransformRsaRipemd160', 32 | 'TransformRsaSha1', 33 | 'TransformRsaPkcs1', 34 | 'TransformMd5', 35 | 'TransformRipemd160', 36 | 'TransformSha1' 37 | ], 38 | } 39 | -------------------------------------------------------------------------------- /docs/linters/DUO119.md: -------------------------------------------------------------------------------- 1 | # DUO119 2 | 3 | This linter looks for use of the `shelve` module. 4 | 5 | From the documentation: 6 | 7 | > Because the `shelve` module is backed by `pickle`, it is insecure to load 8 | > a shelf from an untrusted source. Like with `pickle`, loading a shelf can 9 | > execute arbitrary code. 10 | 11 | ## Problematic code 12 | 13 | ```python 14 | import shelve 15 | ``` 16 | 17 | ## Correct code 18 | 19 | Instead of using the `shelve` library it is preferable to use a serialization 20 | format that does not allow for code execution, such as `json`. 21 | 22 | ## Rationale 23 | 24 | Arbitrary code execution allows an attacker to perform any action within the 25 | context of the system the bug is found. E.g. open a reverse shell to a system 26 | of their choosing, install malware by downloading and running a payload, 27 | silently log actions performed on the victim system, etc. 28 | 29 | Arbitrary code execution bugs are effectively the keys to the castle. We'd 30 | like to avoid using the above function because it commonly allows for these 31 | types of bugs. 32 | 33 | ## Exceptions 34 | 35 | * Code may be safe if data passed to `shelve` contains no user input 36 | * Code may be safe if data passed to `shelve` is a constant string 37 | -------------------------------------------------------------------------------- /docs/linters/DUO120.md: -------------------------------------------------------------------------------- 1 | # DUO120 2 | 3 | This linter looks for use of the `marshal` module. 4 | 5 | From the documentation: 6 | 7 | > The `marshal` module is not intended to be secure against erroneous 8 | > or maliciously constructed data. Never unmarshal data received from an 9 | > untrusted or unauthenticated source. 10 | 11 | ## Problematic code 12 | 13 | ```python 14 | import marshal 15 | ``` 16 | 17 | ## Correct code 18 | 19 | Instead of using the `marshal` library it is preferable to use a serialization 20 | format that does not allow for code execution, such as `json`. 21 | 22 | ## Rationale 23 | 24 | Arbitrary code execution allows an attacker to perform any action within the 25 | context of the system the bug is found. E.g. open a reverse shell to a system 26 | of their choosing, install malware by downloading and running a payload, 27 | silently log actions performed on the victim system, etc. 28 | 29 | Arbitrary code execution bugs are effectively the keys to the castle. We'd 30 | like to avoid using the above function because it commonly allows for these 31 | types of bugs. 32 | 33 | ## Exceptions 34 | 35 | * Code may be safe if data passed to `marshal` contains no user input 36 | * Code may be safe if data passed to `marshal` is a constant string 37 | -------------------------------------------------------------------------------- /docs/linters/DUO103.md: -------------------------------------------------------------------------------- 1 | # DUO103 2 | 3 | This linter searches for insecure use of the `pickle` module. 4 | 5 | Use of `loads`, `load`, and `Unpickler` commonly allows for arbitrary code 6 | execution bugs when combined with user input. 7 | 8 | ## Problematic code 9 | 10 | ```python 11 | import pickle 12 | 13 | malicious_user_input = b"csubprocess\ncheck_output\n(S'ls'\ntR." 14 | pickle.loads(malicious_user_input) 15 | ``` 16 | 17 | ## Correct code 18 | 19 | Instead of using the `pickle` library it is preferable to use a serialization 20 | format that does not allow for code execution, such as `json`. 21 | 22 | ## Rationale 23 | 24 | Arbitrary code execution allows an attacker to perform any action within the 25 | context of the system the bug is found. E.g. open a reverse shell to a system 26 | of their choosing, install malware by downloading and running a payload, 27 | silently log actions performed on the victim system, etc. 28 | 29 | Arbitrary code execution bugs are effectively the keys to the castle. We'd 30 | like to avoid using the above function because it commonly allows for these 31 | types of bugs. 32 | 33 | ## Exceptions 34 | 35 | * Code may be safe if data passed to `pickle` contains no user input 36 | * Code may be safe if data passed to `pickle` is a constant string 37 | -------------------------------------------------------------------------------- /docs/linters/DUO105.md: -------------------------------------------------------------------------------- 1 | # DUO105 2 | 3 | This linter searches for use of the built-in `exec` function. This function 4 | commonly allows for arbitrary code execution bugs when combined with user 5 | input. 6 | 7 | ## Problematic code 8 | 9 | ```python 10 | malicious_user_input = 'open("/etc/passwd", "w").write("bad data")' 11 | 12 | # Python 3 13 | exec(malicious_user_input) 14 | 15 | # Python 2 16 | exec malicious_user_input 17 | ``` 18 | 19 | ## Correct code 20 | 21 | ```python 22 | def touch_file(): 23 | # Constant argument 24 | exec('open("/path/to/filename", "w").write("")') 25 | ``` 26 | 27 | ## Rationale 28 | 29 | Arbitrary code execution allows an attacker to perform any action within the 30 | context of the system the bug is found. E.g. open a reverse shell to a system 31 | of their choosing, install malware by downloading and running a payload, 32 | silently log actions performed on the victim system, etc. 33 | 34 | Arbitrary code execution bugs are effectively the keys to the castle. We'd 35 | like to avoid using the above function because it commonly allows for these 36 | types of bugs. 37 | 38 | 39 | ## Exceptions 40 | 41 | * Code may be safe if data passed to `exec` contains no user input 42 | * Code may be safe if data passed to `exec` is a constant string 43 | -------------------------------------------------------------------------------- /tests/test_bad_hashlib_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadHashlibUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_hashlib_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import hashlib 21 | 22 | var = 'echo "TEST"' 23 | 24 | m1 = hashlib.md5() 25 | m2 = hashlib.sha1() 26 | """ 27 | ) 28 | 29 | linter = dlint.linters.BadHashlibUseLinter() 30 | linter.visit(python_node) 31 | 32 | result = linter.get_results() 33 | expected = [ 34 | dlint.linters.base.Flake8Result( 35 | lineno=6, 36 | col_offset=5, 37 | message=dlint.linters.BadHashlibUseLinter._error_tmpl 38 | ), 39 | dlint.linters.base.Flake8Result( 40 | lineno=7, 41 | col_offset=5, 42 | message=dlint.linters.BadHashlibUseLinter._error_tmpl 43 | ), 44 | ] 45 | 46 | assert result == expected 47 | 48 | 49 | if __name__ == "__main__": 50 | unittest.main() 51 | -------------------------------------------------------------------------------- /docs/linters/DUO109.md: -------------------------------------------------------------------------------- 1 | # DUO109 2 | 3 | This linter searches for insecure use of the `yaml` module. Its parsing 4 | functions `dump`, `dump_all`, `load`, and `load_all` allow for arbitrary code 5 | execution. Thus, they should be avoided in favor of their safe equivalents: 6 | `safe_dump`, `safe_dump_all`, `safe_load`, and `safe_load_all`. 7 | 8 | ## Problematic code 9 | 10 | ```python 11 | import yaml 12 | 13 | malicious_user_input = "your_files: !!python/object/apply:subprocess.check_output ['ls']" 14 | yaml.load(malicious_user_input) 15 | ``` 16 | 17 | ## Correct code 18 | 19 | ```python 20 | import yaml 21 | 22 | malicious_user_input = "your_files: !!python/object/apply:subprocess.check_output ['ls']" 23 | yaml.safe_load(malicious_user_input) 24 | ``` 25 | 26 | ## Rationale 27 | 28 | Arbitrary code execution allows an attacker to perform any action within the 29 | context of the system the bug is found. E.g. open a reverse shell to a system 30 | of their choosing, install malware by downloading and running a payload, 31 | silently log actions performed on the victim system, etc. 32 | 33 | Arbitrary code execution bugs are effectively the keys to the castle. We'd 34 | like to avoid using the above function because it commonly allows for these 35 | types of bugs. 36 | 37 | 38 | ## Exceptions 39 | 40 | None 41 | -------------------------------------------------------------------------------- /dlint/linters/bad_cryptography_module_attribute_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_module_attribute_use 11 | 12 | 13 | class BadCryptographyModuleAttributeUseLinter(bad_module_attribute_use.BadModuleAttributeUseLinter): 14 | """This linter looks for unsafe use of cryptography module attributes. 15 | These attributes may indicate weaknesses in cryptographic operations. 16 | """ 17 | off_by_default = False 18 | 19 | _code = 'DUO134' 20 | _error_tmpl = 'DUO134 insecure "cryptography" attribute use' 21 | 22 | @property 23 | def illegal_module_attributes(self): 24 | return { 25 | 'cryptography.hazmat.primitives.hashes': [ 26 | 'MD5', 27 | 'SHA1', 28 | ], 29 | 'cryptography.hazmat.primitives.ciphers.modes': [ 30 | 'ECB', 31 | ], 32 | 'cryptography.hazmat.primitives.ciphers.algorithms': [ 33 | 'Blowfish', 34 | 'ARC4', 35 | 'IDEA', 36 | ], 37 | 'cryptography.hazmat.primitives.asymmetric.padding': [ 38 | 'PKCS1v15', 39 | ], 40 | } 41 | -------------------------------------------------------------------------------- /dlint/linters/bad_xmlrpc_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_kwarg_use 11 | 12 | from .. import tree 13 | 14 | 15 | class BadXmlrpcUseLinter(bad_kwarg_use.BadKwargUseLinter): 16 | """This linter looks for unsage usage of the 17 | "SimpleXMLRPCServer.register_instance" function. Note that in Python 3 18 | this module is called "xmlrpc.server", but this linter still works because 19 | the attribute name is the same. Unsafe usage looks like: 20 | 21 | "Enabling the allow_dotted_names option allows intruders to access your 22 | module's global variables and may allow intruders to execute arbitrary 23 | code on your machine. Only use this option on a secure, closed network." 24 | 25 | https://docs.python.org/2/library/simplexmlrpcserver.html 26 | """ 27 | off_by_default = False 28 | 29 | _code = 'DUO124' 30 | _error_tmpl = 'DUO124 instance with "allow_dotted_names" enabled is insecure' 31 | 32 | @property 33 | def kwargs(self): 34 | return [ 35 | { 36 | "module_path": "SimpleXMLRPCServer.register_instance", 37 | "kwarg_name": "allow_dotted_names", 38 | "predicate": tree.kwarg_true, 39 | }, 40 | ] 41 | -------------------------------------------------------------------------------- /docs/linters/DUO121.md: -------------------------------------------------------------------------------- 1 | # DUO121 2 | 3 | This linter searches for use of the `tempfile.mktemp` function. This function 4 | may introduce race conditions into your code which could negatively impact 5 | security. 6 | 7 | From the documentation: 8 | 9 | > Use of this function may introduce a security hole in your program. By 10 | > the time you get around to doing anything with the file name it 11 | > returns, someone else may have beaten you to the punch. `mktemp()` usage 12 | > can be replaced easily with `NamedTemporaryFile()`, passing it the 13 | > `delete=False` parameter. 14 | 15 | ## Problematic code 16 | 17 | ```python 18 | import tempfile 19 | 20 | temp_filename = tempfile.mktemp() 21 | ``` 22 | 23 | ## Correct code 24 | 25 | ```python 26 | import tempfile 27 | 28 | fd = tempfile.NamedTemporaryFile(delete=False) 29 | temp_filename = fd.name 30 | ``` 31 | 32 | ## Rationale 33 | 34 | This function introduces a race condition between filename creation time and 35 | file access time. Depending on the code using the created filename, there could 36 | be security implications. For example, denial-of-service (DoS) and 37 | time-of-check-to-time-of-use (TOCTOU) bugs. Consider a situation where a file 38 | created using this function did not exist, but by the time the filename is used 39 | the file exists. Code that tries to create a new file at this location could 40 | crash and lead to a DoS. 41 | 42 | ## Exceptions 43 | 44 | None 45 | -------------------------------------------------------------------------------- /dlint/linters/twisted/returnvalue_in_inlinecallbacks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import ast 11 | 12 | from .. import base 13 | from ... import tree 14 | 15 | 16 | class ReturnValueInInlineCallbacksLinter(base.BaseLinter): 17 | """This linter looks for returnValue calls that are in a function missing 18 | a inlineCallbacks decorator. 19 | """ 20 | off_by_default = False 21 | 22 | _code = 'DUO114' 23 | _error_tmpl = 'DUO114 "returnValue" in function missing "inlineCallbacks" decorator' 24 | 25 | def visit_FunctionDef(self, node): 26 | self.generic_visit(node) 27 | 28 | if tree.function_has_inlinecallbacks_decorator(node): 29 | return 30 | 31 | results = [] 32 | 33 | def returnvalue_statement_callback(inner_node): 34 | if isinstance(inner_node, ast.Call) and tree.call_is_returnvalue(inner_node.func): 35 | results.append(inner_node) 36 | 37 | tree.walk_callback_same_scope(node, returnvalue_statement_callback) 38 | 39 | if results: 40 | self.results.append( 41 | base.Flake8Result( 42 | lineno=node.lineno, 43 | col_offset=node.col_offset, 44 | message=self._error_tmpl 45 | ) 46 | ) 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .coverage.* 40 | .cache 41 | nosetests.xml 42 | coverage.xml 43 | *,cover 44 | .hypothesis/ 45 | 46 | # Translations 47 | *.mo 48 | *.pot 49 | 50 | # Django stuff: 51 | *.log 52 | local_settings.py 53 | 54 | # Flask stuff: 55 | instance/ 56 | .webassets-cache 57 | 58 | # Scrapy stuff: 59 | .scrapy 60 | 61 | # Sphinx documentation 62 | docs/_build/ 63 | 64 | # PyBuilder 65 | target/ 66 | 67 | # IPython Notebook 68 | .ipynb_checkpoints 69 | 70 | # pyenv 71 | .python-version 72 | 73 | # celery beat schedule file 74 | celerybeat-schedule 75 | 76 | # dotenv 77 | .env 78 | 79 | # virtualenv 80 | venv/ 81 | ENV/ 82 | 83 | # Spyder project settings 84 | .spyderproject 85 | 86 | # Rope project settings 87 | .ropeproject 88 | 89 | # Vim swap files 90 | *.sw[po] 91 | -------------------------------------------------------------------------------- /tests/test_bad_sys_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadSysUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_sys_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import sys 21 | 22 | sys.call_tracing(lambda: 42, ()) 23 | sys.setprofile(lambda: 42) 24 | sys.settrace(lambda: 42) 25 | """ 26 | ) 27 | 28 | linter = dlint.linters.BadSysUseLinter() 29 | linter.visit(python_node) 30 | 31 | result = linter.get_results() 32 | expected = [ 33 | dlint.linters.base.Flake8Result( 34 | lineno=4, 35 | col_offset=0, 36 | message=dlint.linters.BadSysUseLinter._error_tmpl 37 | ), 38 | dlint.linters.base.Flake8Result( 39 | lineno=5, 40 | col_offset=0, 41 | message=dlint.linters.BadSysUseLinter._error_tmpl 42 | ), 43 | dlint.linters.base.Flake8Result( 44 | lineno=6, 45 | col_offset=0, 46 | message=dlint.linters.BadSysUseLinter._error_tmpl 47 | ), 48 | ] 49 | 50 | assert result == expected 51 | 52 | 53 | if __name__ == "__main__": 54 | unittest.main() 55 | -------------------------------------------------------------------------------- /docs/linters/DUO132.md: -------------------------------------------------------------------------------- 1 | # DUO132 2 | 3 | This linter searches for insecure keyword argument use in the `urllib3` module. 4 | Specifically, it looks for objects that may have HTTPS certification 5 | verification disabled by setting `"NONE"`, `"CERT_NONE"`, `CERT_NONE`, or 6 | `ssl.CERT_NONE` set for the `cert_reqs` keyward argument. These values disable 7 | verification and thus allow for insecure HTTPS connections. 8 | 9 | ## Problematic code 10 | 11 | ```python 12 | import urllib3 13 | import ssl 14 | from ssl import CERT_NONE 15 | 16 | urllib3.PoolManager(cert_reqs="CERT_NONE") 17 | urllib3.ProxyManager(cert_reqs="CERT_NONE") 18 | urllib3.HTTPSConnectionPool(cert_reqs="NONE") 19 | urllib3.connection_from_url(cert_reqs=ssl.CERT_NONE) 20 | urllib3.proxy_from_url(cert_reqs=CERT_NONE) 21 | ``` 22 | 23 | ## Correct code 24 | 25 | Simply not specifying `cert_reqs` will default to secure behavior. Further, 26 | setting `ssl.CERT_REQUIRED` or its variants will ensure verification is 27 | performed. 28 | 29 | ## Rationale 30 | 31 | HTTPS certificate verification ensures your requests are communicating with 32 | whom they're supposed to be. Without this there could be an attacker 33 | impersonating the server you intend to be communicating with. To prevent this 34 | you must ensure certification verification is enabled. 35 | 36 | ## Exceptions 37 | 38 | * Code connecting to internal network services (although these should strive for full HTTPS as well) 39 | * Code connecting to local development services or in test environments 40 | -------------------------------------------------------------------------------- /docs/linters/DUO106.md: -------------------------------------------------------------------------------- 1 | # DUO106 2 | 3 | This linter searches for insecure use of the `os` module. 4 | 5 | Use of `system`, `popen`, `popen2`, `popen3`, or `popen4` commonly allows for 6 | arbitrary code execution bugs when combined with user input. 7 | 8 | Use of `tempnam` or `tmpnam` is vulnerable to symlink attacks. Consider using 9 | `tmpfile` instead. 10 | 11 | ## Problematic code 12 | 13 | ```python 14 | malicious_input = 'cat /etc/passwd' 15 | os.system(malicious_input) 16 | ``` 17 | 18 | ## Correct code 19 | 20 | Instead of using the `os` module to execute commands, prefer to use the 21 | `subprocess` module while ensuring `shell=False`. 22 | 23 | See [Replacing Older Functions with the `subprocess` Module](https://docs.python.org/3/library/subprocess.html#subprocess-replacements). 24 | 25 | Prefer `tmpfile` to `tempnam` or `tmpnam`. 26 | 27 | ## Rationale 28 | 29 | Arbitrary code execution allows an attacker to perform any action within the 30 | context of the system the bug is found. E.g. open a reverse shell to a system 31 | of their choosing, install malware by downloading and running a payload, 32 | silently log actions performed on the victim system, etc. 33 | 34 | Arbitrary code execution bugs are effectively the keys to the castle. We'd 35 | like to avoid using the above function because it commonly allows for these 36 | types of bugs. 37 | 38 | ## Exceptions 39 | 40 | * Code may be safe if data passed to `os.system` contains no user input 41 | * Code may be safe if data passed to `os.system` is a constant string 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Duo Security 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors 14 | may be used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 21 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 22 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 23 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 24 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 25 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 26 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /docs/linters/DUO135.md: -------------------------------------------------------------------------------- 1 | # DUO135 2 | 3 | This linter searches for insecure use of the `defusedxml` module. This module 4 | provides defenses against common XML attack vectors and thus should be as 5 | locked down as possible. 6 | 7 | ## Problematic code 8 | 9 | ```python 10 | from defusedxml.lxml import parse 11 | from defusedxml.ElementTree import fromstring 12 | from defusedxml.cElementTree import iterparse 13 | 14 | parse("...") 15 | fromstring("...", forbid_entities=False) 16 | iterparse("...", forbid_external=False) 17 | ``` 18 | 19 | ## Correct code 20 | 21 | ```python 22 | from defusedxml.lxml import parse 23 | from defusedxml.ElementTree import fromstring 24 | from defusedxml.cElementTree import iterparse 25 | 26 | parse("...", forbid_dtd=True) 27 | fromstring("...", forbid_dtd=True) 28 | iterparse("...", forbid_dtd=True) 29 | ``` 30 | 31 | Note that `forbid_external` and `forbid_entities` default to `True`, but can 32 | be manually specified as such to ensure secure behavior. 33 | 34 | ## Rationale 35 | 36 | These keyword arguments prevent various XML attack vectors. More specifically, 37 | they prevent DTD retrieval, excessive entity expansion, and external and local 38 | file entity expansion. 39 | 40 | For more information on these and other attack vectors see the [defusedxml docs](https://pypi.org/project/defusedxml/). 41 | 42 | ## Exceptions 43 | 44 | * Situations where the above attack vectors are explicitly required XML 45 | functionality for the program to function correctly. 46 | -------------------------------------------------------------------------------- /dlint/redos/__main__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import argparse 11 | import sys 12 | 13 | from dlint import redos 14 | 15 | 16 | def parse_args(): 17 | p = argparse.ArgumentParser(description=''' 18 | Detect redos regular expression patterns from the command-line. 19 | ''', formatter_class=argparse.RawTextHelpFormatter) 20 | 21 | p.add_argument( 22 | '-p', 23 | '--pattern', 24 | action='store', 25 | required=True, 26 | help='regular expression pattern' 27 | ) 28 | 29 | dump_group = p.add_mutually_exclusive_group() 30 | 31 | dump_group.add_argument( 32 | '-d', 33 | '--dump', 34 | action='store_true', 35 | help='print parsed pattern' 36 | ) 37 | dump_group.add_argument( 38 | '-t', 39 | '--dump-tree', 40 | action='store_true', 41 | help='print parsed op node tree' 42 | ) 43 | 44 | args = p.parse_args() 45 | 46 | return args 47 | 48 | 49 | def main(): 50 | args = parse_args() 51 | 52 | if args.dump: 53 | redos.detect.dump(args.pattern) 54 | return 55 | 56 | if args.dump_tree: 57 | redos.detect.dump_tree(args.pattern) 58 | return 59 | 60 | if args.pattern == '-': 61 | pattern = sys.stdin.read() 62 | else: 63 | pattern = args.pattern 64 | 65 | print((pattern, redos.detect.catastrophic(pattern))) 66 | 67 | 68 | if __name__ == "__main__": 69 | main() 70 | -------------------------------------------------------------------------------- /tests/test_bad_pycrypto_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadPycryptoUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_pycrypto_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import Crypto 21 | """ 22 | ) 23 | 24 | linter = dlint.linters.BadPycryptoUseLinter() 25 | linter.visit(python_node) 26 | 27 | result = linter.get_results() 28 | expected = [ 29 | dlint.linters.base.Flake8Result( 30 | lineno=2, 31 | col_offset=0, 32 | message=dlint.linters.BadPycryptoUseLinter._error_tmpl 33 | ) 34 | ] 35 | 36 | assert result == expected 37 | 38 | def test_bad_pycrypto_from_usage(self): 39 | python_node = self.get_ast_node( 40 | """ 41 | from Crypto import AES 42 | """ 43 | ) 44 | 45 | linter = dlint.linters.BadPycryptoUseLinter() 46 | linter.visit(python_node) 47 | 48 | result = linter.get_results() 49 | expected = [ 50 | dlint.linters.base.Flake8Result( 51 | lineno=2, 52 | col_offset=0, 53 | message=dlint.linters.BadPycryptoUseLinter._error_tmpl 54 | ) 55 | ] 56 | 57 | assert result == expected 58 | 59 | 60 | if __name__ == "__main__": 61 | unittest.main() 62 | -------------------------------------------------------------------------------- /dlint/linters/bad_ssl_module_attribute_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_module_attribute_use 11 | 12 | 13 | class BadSSLModuleAttributeUseLinter(bad_module_attribute_use.BadModuleAttributeUseLinter): 14 | """This linter looks for unsafe use of the Python "ssl" module. Securely 15 | making SSL connections is a difficult task, and is often filled with 16 | gotchas. This linter performs basic sanity checks on various common 17 | mistakes, but still can't guarantee that no findings means usage is safe. 18 | 19 | Further, there may be false positives associated with this rule. For 20 | example, PROTOCOL_SSLv23 allows for ALL protocol versions, including the 21 | secure TLSv1.2, and the insecure SSLv3. PROTOCOL_SSLv23 may be fine in 22 | specific contexts, but its usage may point to code warranting further 23 | investigation. 24 | """ 25 | off_by_default = False 26 | 27 | _code = 'DUO122' 28 | _error_tmpl = 'DUO122 insecure "ssl" module attribute use' 29 | 30 | @property 31 | def illegal_module_attributes(self): 32 | return { 33 | 'ssl': [ 34 | '_create_unverified_context', 35 | '_https_verify_certificates', 36 | 'CERT_NONE', 37 | 'CERT_OPTIONAL', 38 | 'PROTOCOL_SSLv2', 39 | 'PROTOCOL_SSLv23', 40 | 'PROTOCOL_SSLv3', 41 | 'PROTOCOL_TLS', 42 | ], 43 | } 44 | -------------------------------------------------------------------------------- /tests/test_bad_pickle_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadPickleUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_pickle_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import pickle 21 | 22 | var = 'test' 23 | 24 | pickle.loads(var) 25 | 26 | with open('data.pickle', 'r') as f: 27 | pickle.load(f) 28 | up = pickle.Unpickler(f) 29 | up.load() 30 | """ 31 | ) 32 | 33 | linter = dlint.linters.BadPickleUseLinter() 34 | linter.visit(python_node) 35 | 36 | result = linter.get_results() 37 | expected = [ 38 | dlint.linters.base.Flake8Result( 39 | lineno=6, 40 | col_offset=0, 41 | message=dlint.linters.BadPickleUseLinter._error_tmpl 42 | ), 43 | dlint.linters.base.Flake8Result( 44 | lineno=9, 45 | col_offset=4, 46 | message=dlint.linters.BadPickleUseLinter._error_tmpl 47 | ), 48 | dlint.linters.base.Flake8Result( 49 | lineno=10, 50 | col_offset=9, 51 | message=dlint.linters.BadPickleUseLinter._error_tmpl 52 | ) 53 | ] 54 | 55 | assert result == expected 56 | 57 | 58 | if __name__ == "__main__": 59 | unittest.main() 60 | -------------------------------------------------------------------------------- /dlint/linters/twisted/inlinecallbacks_yield_statement.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import ast 11 | 12 | from .. import base 13 | from ... import tree 14 | 15 | 16 | class InlineCallbacksYieldStatementLinter(base.BaseLinter): 17 | """This linter looks for inlineCallbacks functions that are missing a 18 | yield statement. The presence of a yield statement turns a normal function 19 | into a generator. The inlineCallback generator depends on this behavior, 20 | so let's check for cases where it's missing. 21 | """ 22 | off_by_default = False 23 | 24 | _code = 'DUO113' 25 | _error_tmpl = 'DUO113 "inlineCallbacks" function missing "yield" statement' 26 | 27 | def visit_FunctionDef(self, node): 28 | self.generic_visit(node) 29 | 30 | if not tree.function_has_inlinecallbacks_decorator(node): 31 | return 32 | 33 | if tree.function_is_empty(node): 34 | return 35 | 36 | results = [] 37 | 38 | def yield_statement_callback(inner_node): 39 | if isinstance(inner_node, ast.Yield): 40 | results.append(inner_node) 41 | 42 | tree.walk_callback_same_scope(node, yield_statement_callback) 43 | 44 | if not results: 45 | self.results.append( 46 | base.Flake8Result( 47 | lineno=node.lineno, 48 | col_offset=node.col_offset, 49 | message=self._error_tmpl 50 | ) 51 | ) 52 | -------------------------------------------------------------------------------- /docs/linters/DUO110.md: -------------------------------------------------------------------------------- 1 | # DUO110 2 | 3 | This linter searches for use of the built-in `compile` function. This function 4 | is commonly accompanied by `eval` or `exec` and thus could lead to arbitrary 5 | code execution bugs. Further, a sufficiently large string passed to `compile` 6 | can crash the Python interpreter. 7 | 8 | ## Problematic code 9 | 10 | ```python 11 | malicious_input = 'open("/etc/passwd", "w").write("bad data")' 12 | code = compile(malicious_input, '', 'exec') 13 | exec(code) 14 | ``` 15 | 16 | ```python 17 | crash_interpreter = '+chr(33)' * 1000000 18 | compile(crash_interpreter, '', 'eval') 19 | ``` 20 | 21 | ## Correct code 22 | 23 | Ensure data passed to `compile` contains no user input, or is a constant 24 | string, and is limited to small strings. 25 | 26 | ## Rationale 27 | 28 | Arbitrary code execution allows an attacker to perform any action within the 29 | context of the system the bug is found. E.g. open a reverse shell to a system 30 | of their choosing, install malware by downloading and running a payload, 31 | silently log actions performed on the victim system, etc. 32 | 33 | Arbitrary code execution bugs are effectively the keys to the castle. We'd 34 | like to avoid using the above function because it commonly allows for these 35 | types of bugs. 36 | 37 | Further, with a sufficiently large string a user could crash the Python 38 | interpreter which may lead to denial-of-service (DoS) bugs. 39 | 40 | ## Exceptions 41 | 42 | * Code may be safe if data passed to `compile` contains no user input and limits data size 43 | * Code may be safe if data passed to `compile` is a constant string and limits data size 44 | -------------------------------------------------------------------------------- /dlint/linters/twisted/yield_return_statement.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import ast 11 | import sys 12 | 13 | from .. import base 14 | from ... import tree 15 | 16 | 17 | class YieldReturnStatementLinter(base.BaseLinter): 18 | """This linter looks for inlineCallbacks functions that have 19 | non-empty return statements. Using a non-empty return statement and a 20 | yield statement in the same function is a syntax error. 21 | """ 22 | off_by_default = False 23 | 24 | _code = 'DUO101' 25 | _error_tmpl = 'DUO101 "inlineCallbacks" function cannot have non-empty "return" statement' 26 | 27 | def visit_FunctionDef(self, node): 28 | self.generic_visit(node) 29 | 30 | # https://twistedmatrix.com/documents/17.1.0/api/twisted.internet.defer.inlineCallbacks.html 31 | is_python_3_3 = sys.version_info >= (3, 3) 32 | 33 | if (is_python_3_3 34 | or not tree.function_has_inlinecallbacks_decorator(node)): 35 | return 36 | 37 | results = [] 38 | 39 | def return_statement_callback(inner_node): 40 | if isinstance(inner_node, ast.Return) and tree.non_empty_return(inner_node): 41 | results.append(inner_node) 42 | 43 | tree.walk_callback_same_scope(node, return_statement_callback) 44 | 45 | self.results.extend( 46 | base.Flake8Result( 47 | lineno=result.lineno, 48 | col_offset=result.col_offset, 49 | message=self._error_tmpl 50 | ) 51 | for result in results 52 | ) 53 | -------------------------------------------------------------------------------- /tests/test_benchmark/test_benchmark.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import os 11 | import sys 12 | import unittest 13 | 14 | import pytest 15 | 16 | # Since extension imports dlint we cannot add it to the module or else we'll 17 | # have circular imports. Thus we must come up with some tricks to import it 18 | sys.path.append( 19 | os.path.join( 20 | os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 21 | "dlint" 22 | ) 23 | ) 24 | 25 | import extension # noqa: E402 26 | 27 | 28 | def get_single_linter_extension(linter): 29 | class SingleLinterExtention(extension.Flake8Extension): 30 | @classmethod 31 | def get_linter_classes(cls): 32 | return [linter] 33 | 34 | return SingleLinterExtention 35 | 36 | 37 | def test_benchmark_run(benchmark_py_file, benchmark): 38 | ext = extension.Flake8Extension(benchmark_py_file, "unused") 39 | 40 | benchmark(lambda: list(ext.run())) 41 | 42 | assert ext 43 | 44 | 45 | @pytest.mark.parametrize( 46 | 'linter', 47 | sorted(extension.dlint.linters.ALL, key=lambda l: l._code), 48 | ids=lambda l: "{}-{}".format(l._code, l.__name__) 49 | ) 50 | def test_benchmark_individual(benchmark_py_file, benchmark_group_base_class, benchmark, linter): 51 | if benchmark_group_base_class: 52 | benchmark.group = str(linter.__bases__) 53 | 54 | ext_class = get_single_linter_extension(linter) 55 | ext = ext_class(benchmark_py_file, "unused") 56 | 57 | benchmark(lambda: list(ext.run())) 58 | 59 | assert ext 60 | 61 | 62 | if __name__ == "__main__": 63 | unittest.main() 64 | -------------------------------------------------------------------------------- /dlint/linters/bad_input_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import ast 11 | import sys 12 | 13 | from . import base 14 | 15 | 16 | class BadInputUseLinter(base.BaseLinter): 17 | """This linter looks for use of the Python "input" function. In Python 2 18 | this function is tantamount to eval(raw_input()), and thus should not be 19 | used. In Python 3 raw_input() functionality has been moved to input(). 20 | """ 21 | off_by_default = False 22 | 23 | _code = 'DUO108' 24 | _error_tmpl = 'DUO108 use of "input" is insecure' 25 | 26 | def __init__(self, *args, **kwargs): 27 | self.unsafe_input_import = True 28 | 29 | super(BadInputUseLinter, self).__init__(*args, **kwargs) 30 | 31 | def visit_Call(self, node): 32 | is_python_2 = sys.version_info < (3, 0) 33 | 34 | if (is_python_2 35 | and self.unsafe_input_import 36 | and isinstance(node.func, ast.Name) 37 | and node.func.id == 'input'): 38 | self.results.append( 39 | base.Flake8Result( 40 | lineno=node.lineno, 41 | col_offset=node.col_offset, 42 | message=self._error_tmpl 43 | ) 44 | ) 45 | 46 | def visit_ImportFrom(self, node): 47 | # Using input from six.moves is valid, so if input is imported 48 | # in a safe way, allow input to be used for the rest of the file 49 | if (node.module == 'six.moves' 50 | and any(alias.name == 'input' for alias in node.names)): 51 | self.unsafe_input_import = False 52 | -------------------------------------------------------------------------------- /tests/test_bad_exec_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import sys 11 | import unittest 12 | 13 | import dlint 14 | 15 | IS_PYTHON_3 = sys.version_info >= (3, 0) 16 | 17 | 18 | class TestBadExecUse(dlint.test.base.BaseTest): 19 | 20 | def test_bad_exec_usage(self): 21 | python_node = self.get_ast_node( 22 | """ 23 | var = 1 24 | 25 | exec('print var + 1') 26 | """ 27 | ) 28 | 29 | linter = dlint.linters.BadExecUseLinter() 30 | linter.visit(python_node) 31 | 32 | result = linter.get_results() 33 | expected = [ 34 | dlint.linters.base.Flake8Result( 35 | lineno=4, 36 | col_offset=0, 37 | message=dlint.linters.BadExecUseLinter._error_tmpl 38 | ) 39 | ] 40 | 41 | assert result == expected 42 | 43 | @unittest.skipIf(IS_PYTHON_3, 'exec statements are a SyntaxError in Python 3') 44 | def test_bad_exec_statement_usage(self): 45 | python_node = self.get_ast_node( 46 | """ 47 | var = 1 48 | 49 | exec 'print var + 1' 50 | """ 51 | ) 52 | 53 | linter = dlint.linters.BadExecUseLinter() 54 | linter.visit(python_node) 55 | 56 | result = linter.get_results() 57 | expected = [ 58 | dlint.linters.base.Flake8Result( 59 | lineno=4, 60 | col_offset=0, 61 | message=dlint.linters.BadExecUseLinter._error_tmpl 62 | ) 63 | ] 64 | 65 | assert result == expected 66 | 67 | 68 | if __name__ == "__main__": 69 | unittest.main() 70 | -------------------------------------------------------------------------------- /tests/test_bad_urllib3_module_attribute_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadUrllib3ModuleAttributeUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_urllib3_module_attribute_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import urllib3 21 | urllib3.disable_warnings() 22 | """ 23 | ) 24 | 25 | linter = dlint.linters.BadUrllib3ModuleAttributeUseLinter() 26 | linter.visit(python_node) 27 | 28 | result = linter.get_results() 29 | expected = [ 30 | dlint.linters.base.Flake8Result( 31 | lineno=3, 32 | col_offset=0, 33 | message=dlint.linters.BadUrllib3ModuleAttributeUseLinter._error_tmpl 34 | ), 35 | ] 36 | 37 | assert result == expected 38 | 39 | def test_bad_urllib3_module_attribute_usage_from_import(self): 40 | python_node = self.get_ast_node( 41 | """ 42 | from urllib3 import disable_warnings 43 | disable_warnings() 44 | """ 45 | ) 46 | 47 | linter = dlint.linters.BadUrllib3ModuleAttributeUseLinter() 48 | linter.visit(python_node) 49 | 50 | result = linter.get_results() 51 | expected = [ 52 | dlint.linters.base.Flake8Result( 53 | lineno=3, 54 | col_offset=0, 55 | message=dlint.linters.BadUrllib3ModuleAttributeUseLinter._error_tmpl 56 | ), 57 | ] 58 | 59 | assert result == expected 60 | 61 | 62 | if __name__ == "__main__": 63 | unittest.main() 64 | -------------------------------------------------------------------------------- /tests/test_bad_yaml_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadYAMLUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_yaml_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import yaml 21 | 22 | var1 = {'foo': 'bar'} 23 | var2 = 'test: !!python/object/apply:print ["HAI"]' 24 | 25 | yaml.dump(var1) 26 | yaml.dump_all([var1]) 27 | 28 | yaml.load(var2) 29 | yaml.load_all(var2) 30 | """ 31 | ) 32 | 33 | linter = dlint.linters.BadYAMLUseLinter() 34 | linter.visit(python_node) 35 | 36 | result = linter.get_results() 37 | expected = [ 38 | dlint.linters.base.Flake8Result( 39 | lineno=7, 40 | col_offset=0, 41 | message=dlint.linters.BadYAMLUseLinter._error_tmpl 42 | ), 43 | dlint.linters.base.Flake8Result( 44 | lineno=8, 45 | col_offset=0, 46 | message=dlint.linters.BadYAMLUseLinter._error_tmpl 47 | ), 48 | dlint.linters.base.Flake8Result( 49 | lineno=10, 50 | col_offset=0, 51 | message=dlint.linters.BadYAMLUseLinter._error_tmpl 52 | ), 53 | dlint.linters.base.Flake8Result( 54 | lineno=11, 55 | col_offset=0, 56 | message=dlint.linters.BadYAMLUseLinter._error_tmpl 57 | ), 58 | ] 59 | 60 | assert result == expected 61 | 62 | 63 | if __name__ == "__main__": 64 | unittest.main() 65 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import ( 2 | find_packages, 3 | setup, 4 | ) 5 | 6 | import os 7 | 8 | import dlint 9 | 10 | requirements_filename = os.path.join( 11 | os.path.dirname(os.path.abspath(__file__)), 'requirements.txt') 12 | 13 | with open(requirements_filename) as fd: 14 | install_requires = [i.strip() for i in fd.readlines()] 15 | 16 | requirements_dev_filename = os.path.join( 17 | os.path.dirname(os.path.abspath(__file__)), 'requirements-dev.txt') 18 | 19 | with open(requirements_dev_filename) as fd: 20 | tests_require = [i.strip() for i in fd.readlines()] 21 | 22 | long_description_filename = os.path.join( 23 | os.path.dirname(os.path.abspath(__file__)), 'README.md') 24 | 25 | with open(long_description_filename) as fd: 26 | long_description = fd.read() 27 | 28 | setup( 29 | name=dlint.__name__, 30 | version=dlint.__version__, 31 | description=dlint.__description__, 32 | long_description=long_description, 33 | long_description_content_type='text/markdown', 34 | url=dlint.__url__, 35 | packages=find_packages(), 36 | license=dlint.__license__, 37 | classifiers=[ 38 | 'Environment :: Console', 39 | 'License :: OSI Approved :: BSD License', 40 | 'Operating System :: MacOS :: MacOS X', 41 | 'Operating System :: Microsoft :: Windows', 42 | 'Operating System :: POSIX', 43 | 'Programming Language :: Python :: 2.7', 44 | 'Programming Language :: Python :: 3.5', 45 | 'Programming Language :: Python :: 3.6', 46 | 'Programming Language :: Python :: 3.7', 47 | 'Programming Language :: Python :: 3.8', 48 | 'Topic :: Security', 49 | 'Topic :: Software Development :: Quality Assurance', 50 | ], 51 | tests_require=tests_require, 52 | install_requires=install_requires, 53 | entry_points={ 54 | 'flake8.extension': [ 55 | 'DUO = dlint.extension:Flake8Extension', 56 | ], 57 | }, 58 | ) 59 | -------------------------------------------------------------------------------- /dlint/linters/bad_subprocess_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_kwarg_use 11 | 12 | from .. import tree 13 | 14 | 15 | class BadSubprocessUseLinter(bad_kwarg_use.BadKwargUseLinter): 16 | """This linter looks for use of the "shell=True" kwarg when using the 17 | "subprocess" module. 18 | 19 | "If the shell is invoked explicitly, via shell=True, it is the 20 | application's responsibility to ensure that all whitespace and 21 | metacharacters are quoted appropriately to avoid shell injection 22 | vulnerabilities." 23 | 24 | https://docs.python.org/3.6/library/subprocess.html#security-considerations 25 | """ 26 | off_by_default = False 27 | 28 | _code = 'DUO116' 29 | _error_tmpl = 'DUO116 use of "shell=True" is insecure in "subprocess" module' 30 | 31 | @property 32 | def kwargs(self): 33 | return [ 34 | { 35 | "module_path": "subprocess.call", 36 | "kwarg_name": "shell", 37 | "predicate": tree.kwarg_present, 38 | }, 39 | { 40 | "module_path": "subprocess.check_call", 41 | "kwarg_name": "shell", 42 | "predicate": tree.kwarg_present, 43 | }, 44 | { 45 | "module_path": "subprocess.check_output", 46 | "kwarg_name": "shell", 47 | "predicate": tree.kwarg_present, 48 | }, 49 | { 50 | "module_path": "subprocess.Popen", 51 | "kwarg_name": "shell", 52 | "predicate": tree.kwarg_present, 53 | }, 54 | { 55 | "module_path": "subprocess.run", 56 | "kwarg_name": "shell", 57 | "predicate": tree.kwarg_present, 58 | } 59 | ] 60 | -------------------------------------------------------------------------------- /docs/linters/DUO137.md: -------------------------------------------------------------------------------- 1 | # DUO137 2 | 3 | This linter searches for insecure keyword argument use in the `itsdangerous` 4 | library. Specifically, it looks for signing operations using the none algorithm 5 | which results in empty signatures. 6 | 7 | ## Problematic code 8 | 9 | ```python 10 | >>> import itsdangerous 11 | >>> s1 = itsdangerous.signer.Signer("key1", algorithm=itsdangerous.signer.NoneAlgorithm()) 12 | >>> s2 = itsdangerous.signer.Signer("key2", algorithm=itsdangerous.signer.NoneAlgorithm()) 13 | >>> signature = s1.sign("foo") 14 | >>> s2.unsign(signature) 15 | foo 16 | ``` 17 | 18 | The following usages of the none algorithm are insecure: 19 | 20 | ```python 21 | itsdangerous.signer.Signer("key", algorithm=itsdangerous.signer.NoneAlgorithm()) 22 | itsdangerous.signer.Signer("key", algorithm=itsdangerous.NoneAlgorithm()) 23 | itsdangerous.Signer("key", algorithm=itsdangerous.NoneAlgorithm()) 24 | itsdangerous.Signer("key", algorithm=itsdangerous.signer.NoneAlgorithm()) 25 | 26 | itsdangerous.timed.TimestampSigner("key", algorithm=itsdangerous.signer.NoneAlgorithm()) 27 | itsdangerous.timed.TimestampSigner("key", algorithm=itsdangerous.NoneAlgorithm()) 28 | itsdangerous.TimestampSigner("key", algorithm=itsdangerous.NoneAlgorithm()) 29 | itsdangerous.TimestampSigner("key", algorithm=itsdangerous.signer.NoneAlgorithm()) 30 | 31 | itsdangerous.jws.JSONWebSignatureSerializer("key", algorithm_name="none") 32 | itsdangerous.JSONWebSignatureSerializer("key", algorithm_name="none") 33 | ``` 34 | 35 | ## Correct code 36 | 37 | Simply not specifying `algorithm|algorithm_name` will default to secure 38 | behavior. Further, setting `HMACAlgorithm` will ensure verification is 39 | performed. 40 | 41 | ## Rationale 42 | 43 | Setting the algorithm to none turns off signature verification. This breaks 44 | HMAC security. For more information see 45 | [Critical vulnerabilities in JSON Web Token libraries](https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/). 46 | 47 | ## Exceptions 48 | 49 | None 50 | -------------------------------------------------------------------------------- /docs/linters/DUO128.md: -------------------------------------------------------------------------------- 1 | # DUO128 2 | 3 | This linter searches for insecure keyword argument use of the `onelogin` module. 4 | 5 | The `onelogin` module provides [SAML](https://en.wikipedia.org/wiki/Security_Assertion_Markup_Language) 6 | functionality in a Python library. This linter searches for many insecure ways 7 | of using the SAML protocol. This includes things like using SHA1 as a 8 | fingerprint algorithm (as opposed to SHA256), SHA1 signatures used during 9 | signing/validating, disabled validation, etc. 10 | 11 | ## Problematic code 12 | 13 | Due to the number of behaviors this linter searches for it is recommended to 14 | look at the [linter code itself](https://github.com/dlint-py/dlint/blob/master/dlint/linters/bad_onelogin_kwarg_use.py). 15 | 16 | ## Correct code 17 | 18 | SAML is a notoriously difficult protocol to implement and use correctly. It is 19 | not feasible to show a secure SAML implementation that fits many cases 20 | generally. 21 | 22 | Be sure that you're familiar with the `security` settings in the [How it Works](https://github.com/onelogin/python-saml#how-it-works) 23 | section and common literature for securing this protocol: 24 | 25 | * [The Beer Drinker's Guide to SAML](https://duo.com/blog/the-beer-drinkers-guide-to-saml) 26 | * [How SAML Authentication Works](https://auth0.com/blog/how-saml-authentication-works/) 27 | * [SAML Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SAML_Security_Cheat_Sheet.html) 28 | * [On Breaking SAML: Be Whoever You Want to Be](https://www.usenix.org/system/files/conference/usenixsecurity12/sec12-final91.pdf) 29 | 30 | ## Rationale 31 | 32 | See above. 33 | 34 | ## Exceptions 35 | 36 | * There will often be times when using the most secure options will not be 37 | possible. For example, the SP or IDP you're communicating with does not support 38 | the most secure configuration. In these situations the challenges may not be 39 | completely technical ones and you may have to fallback on insecure (or at 40 | least not *the most* secure) options - here you'll have to use your best 41 | judgement. 42 | -------------------------------------------------------------------------------- /tests/test_bad_subprocess_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadSubprocessUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_subprocess_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import subprocess 21 | 22 | subprocess.call(shell=True) 23 | subprocess.check_call(shell=True) 24 | subprocess.check_output(shell=True) 25 | subprocess.Popen(shell=True) 26 | subprocess.run(shell=True) 27 | """ 28 | ) 29 | 30 | linter = dlint.linters.BadSubprocessUseLinter() 31 | linter.visit(python_node) 32 | 33 | result = linter.get_results() 34 | expected = [ 35 | dlint.linters.base.Flake8Result( 36 | lineno=4, 37 | col_offset=0, 38 | message=dlint.linters.BadSubprocessUseLinter._error_tmpl 39 | ), 40 | dlint.linters.base.Flake8Result( 41 | lineno=5, 42 | col_offset=0, 43 | message=dlint.linters.BadSubprocessUseLinter._error_tmpl 44 | ), 45 | dlint.linters.base.Flake8Result( 46 | lineno=6, 47 | col_offset=0, 48 | message=dlint.linters.BadSubprocessUseLinter._error_tmpl 49 | ), 50 | dlint.linters.base.Flake8Result( 51 | lineno=7, 52 | col_offset=0, 53 | message=dlint.linters.BadSubprocessUseLinter._error_tmpl 54 | ), 55 | dlint.linters.base.Flake8Result( 56 | lineno=8, 57 | col_offset=0, 58 | message=dlint.linters.BadSubprocessUseLinter._error_tmpl 59 | ), 60 | ] 61 | 62 | assert result == expected 63 | 64 | 65 | if __name__ == "__main__": 66 | unittest.main() 67 | -------------------------------------------------------------------------------- /dlint/multi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import ast 11 | 12 | from . import namespace 13 | 14 | 15 | class MultiNodeVisitor(ast.NodeVisitor): 16 | def __init__(self, linters, *args, **kwargs): 17 | self.linters = linters 18 | self.namespace = None 19 | 20 | super(MultiNodeVisitor, self).__init__(*args, **kwargs) 21 | 22 | def visit(self, node): 23 | if not self.namespace: 24 | # Assuming that 'visit' is always called the first time with 'node' 25 | # as an ast.Module. If not, we should fail fast with a raised 26 | # exception and investigate. 27 | self.namespace = namespace.Namespace.from_module_node(node) 28 | for linter in self.linters: 29 | linter.namespace = self.namespace 30 | 31 | # Python 2 cannot rebind free variables, i.e. 'nonlocal' 32 | nonlocal_hack = {'any_recurse': False} 33 | 34 | def recurse_visit(inner_node): 35 | # To avoid having each linter call its own 'generic_visit' we 36 | # update the linters to report back whether they would've called 37 | # the function or not. 38 | nonlocal_hack['any_recurse'] = True 39 | 40 | for linter in self.linters: 41 | linter.generic_visit = recurse_visit 42 | 43 | method = 'visit_' + node.__class__.__name__ 44 | 45 | # Only recurse further down the AST once if ANY of the linters doesn't 46 | # implement a visit function. This is optimized over recursing down the 47 | # AST for ALL linters. 48 | any_generic = False 49 | for linter in self.linters: 50 | visitor = getattr(linter, method, None) 51 | 52 | if visitor is None: 53 | any_generic = True 54 | continue 55 | 56 | visitor(node) 57 | 58 | if any_generic or nonlocal_hack['any_recurse']: 59 | self.generic_visit(node) 60 | -------------------------------------------------------------------------------- /tests/test_bad_urllib3_kwarg_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadUrllib3KwargUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_urllib3_kwarg_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import urllib3 21 | import ssl 22 | from ssl import CERT_NONE 23 | 24 | urllib3.PoolManager(cert_reqs="CERT_NONE") 25 | urllib3.ProxyManager(cert_reqs="CERT_NONE") 26 | urllib3.HTTPSConnectionPool(cert_reqs="NONE") 27 | urllib3.connection_from_url(cert_reqs=ssl.CERT_NONE) 28 | urllib3.proxy_from_url(cert_reqs=CERT_NONE) 29 | """ 30 | ) 31 | 32 | linter = dlint.linters.BadUrllib3KwargUseLinter() 33 | linter.visit(python_node) 34 | 35 | result = linter.get_results() 36 | expected = [ 37 | dlint.linters.base.Flake8Result( 38 | lineno=6, 39 | col_offset=0, 40 | message=dlint.linters.BadUrllib3KwargUseLinter._error_tmpl 41 | ), 42 | dlint.linters.base.Flake8Result( 43 | lineno=7, 44 | col_offset=0, 45 | message=dlint.linters.BadUrllib3KwargUseLinter._error_tmpl 46 | ), 47 | dlint.linters.base.Flake8Result( 48 | lineno=8, 49 | col_offset=0, 50 | message=dlint.linters.BadUrllib3KwargUseLinter._error_tmpl 51 | ), 52 | dlint.linters.base.Flake8Result( 53 | lineno=9, 54 | col_offset=0, 55 | message=dlint.linters.BadUrllib3KwargUseLinter._error_tmpl 56 | ), 57 | dlint.linters.base.Flake8Result( 58 | lineno=10, 59 | col_offset=0, 60 | message=dlint.linters.BadUrllib3KwargUseLinter._error_tmpl 61 | ), 62 | ] 63 | 64 | assert result == expected 65 | 66 | 67 | if __name__ == "__main__": 68 | unittest.main() 69 | -------------------------------------------------------------------------------- /tests/test_bad_xml_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadXMLUse(dlint.test.base.BaseTest): 16 | 17 | def test_xml_import_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import xml 21 | import xmlrpclib 22 | import lxml 23 | """ 24 | ) 25 | 26 | linter = dlint.linters.BadXMLUseLinter() 27 | linter.visit(python_node) 28 | 29 | result = linter.get_results() 30 | expected = [ 31 | dlint.linters.base.Flake8Result( 32 | lineno=2, 33 | col_offset=0, 34 | message=dlint.linters.BadXMLUseLinter._error_tmpl 35 | ), 36 | dlint.linters.base.Flake8Result( 37 | lineno=3, 38 | col_offset=0, 39 | message=dlint.linters.BadXMLUseLinter._error_tmpl 40 | ), 41 | dlint.linters.base.Flake8Result( 42 | lineno=4, 43 | col_offset=0, 44 | message=dlint.linters.BadXMLUseLinter._error_tmpl 45 | ) 46 | ] 47 | 48 | assert result == expected 49 | 50 | def test_saxutils_import_usage(self): 51 | python_node = self.get_ast_node( 52 | """ 53 | import xml.sax.saxutils 54 | from xml.sax import saxutils 55 | """ 56 | ) 57 | 58 | linter = dlint.linters.BadXMLUseLinter() 59 | linter.visit(python_node) 60 | 61 | result = linter.get_results() 62 | expected = [] 63 | 64 | assert result == expected 65 | 66 | def test_defused_lxml_usage(self): 67 | python_node = self.get_ast_node( 68 | """ 69 | from defusedxml import lxml 70 | """ 71 | ) 72 | 73 | linter = dlint.linters.BadXMLUseLinter() 74 | linter.visit(python_node) 75 | 76 | result = linter.get_results() 77 | expected = [] 78 | 79 | assert result == expected 80 | 81 | 82 | if __name__ == "__main__": 83 | unittest.main() 84 | -------------------------------------------------------------------------------- /docs/linters/DUO129.md: -------------------------------------------------------------------------------- 1 | # DUO129 2 | 3 | This linter searches for insecure attribute use of the `onelogin` module. 4 | 5 | The `onelogin` module provides [SAML](https://en.wikipedia.org/wiki/Security_Assertion_Markup_Language) 6 | functionality in a Python library. This linter searches for many insecure ways 7 | of using the SAML protocol. This includes things like using SHA1 or Triple DES 8 | during signing or encrypting operations. 9 | 10 | ## Problematic code 11 | 12 | ```python 13 | import onelogin.saml2.utils.OneLogin_Saml2_Constants 14 | 15 | onelogin.saml2.utils.OneLogin_Saml2_Constants.SHA1 16 | onelogin.saml2.utils.OneLogin_Saml2_Constants.RSA_SHA1 17 | onelogin.saml2.utils.OneLogin_Saml2_Constants.DSA_SHA1 18 | onelogin.saml2.utils.OneLogin_Saml2_Constants.TRIPLEDES_CBC 19 | ``` 20 | 21 | ## Correct code 22 | 23 | ```python 24 | import onelogin.saml2.utils.OneLogin_Saml2_Constants 25 | 26 | onelogin.saml2.utils.OneLogin_Saml2_Constants.SHA256 27 | onelogin.saml2.utils.OneLogin_Saml2_Constants.RSA_SHA256 28 | ``` 29 | 30 | ## Rationale 31 | 32 | SAML is a notoriously difficult protocol to implement and use correctly. It is 33 | not feasible to show a secure SAML implementation that fits many cases 34 | generally. 35 | 36 | Be sure that you're familiar with the `security` settings in the [How it Works](https://github.com/onelogin/python-saml#how-it-works) 37 | section and common literature for securing this protocol: 38 | 39 | * [The Beer Drinker's Guide to SAML](https://duo.com/blog/the-beer-drinkers-guide-to-saml) 40 | * [How SAML Authentication Works](https://auth0.com/blog/how-saml-authentication-works/) 41 | * [SAML Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SAML_Security_Cheat_Sheet.html) 42 | * [On Breaking SAML: Be Whoever You Want to Be](https://www.usenix.org/system/files/conference/usenixsecurity12/sec12-final91.pdf) 43 | 44 | ## Exceptions 45 | 46 | * There will often be times when using the most secure options will not be 47 | possible. For example, the SP or IDP you're communicating with does not support 48 | the most secure configuration. In these situations the challenges may not be 49 | completely technical ones and you may have to fallback on insecure (or at 50 | least not *the most* secure) options - here you'll have to use your best 51 | judgement. 52 | -------------------------------------------------------------------------------- /dlint/linters/bad_random_generator_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import ast 11 | 12 | from . import base 13 | 14 | 15 | class BadRandomGeneratorUseLinter(base.BaseLinter): 16 | """This linter looks for any use of the Python "random" module EXCEPT 17 | SystemRandom. 18 | 19 | By default Python uses a Mersenne Twister[1] implementation to generate 20 | random values, and this is not suitable for cryptographic purposes. 21 | SystemRandom uses os.urandom to get random data, which is generally a 22 | much better choice. 23 | 24 | [1] https://en.wikipedia.org/wiki/Mersenne_twister 25 | """ 26 | off_by_default = False 27 | 28 | _code = 'DUO102' 29 | _error_tmpl = 'DUO102 insecure use of "random" module, prefer "random.SystemRandom"' 30 | 31 | def visit_Attribute(self, node): 32 | legal_module_functions = [ 33 | ('random', 'SystemRandom'), 34 | ] 35 | 36 | if (isinstance(node.value, ast.Name)): 37 | illegal_function_use = any( 38 | node.value.id == module and node.attr != function 39 | for module, function in legal_module_functions 40 | ) 41 | 42 | if illegal_function_use: 43 | self.results.append( 44 | base.Flake8Result( 45 | lineno=node.lineno, 46 | col_offset=node.col_offset, 47 | message=self._error_tmpl 48 | ) 49 | ) 50 | 51 | def visit_ImportFrom(self, node): 52 | legal_module_functions = [ 53 | ('random', 'SystemRandom'), 54 | ] 55 | illegal_import_from_use = any( 56 | node.module == module and any(alias.name != function for alias in node.names) 57 | for module, function in legal_module_functions 58 | ) 59 | 60 | if illegal_import_from_use: 61 | self.results.append( 62 | base.Flake8Result( 63 | lineno=node.lineno, 64 | col_offset=node.col_offset, 65 | message=self._error_tmpl 66 | ) 67 | ) 68 | -------------------------------------------------------------------------------- /dlint/linters/bad_requests_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_kwarg_use 11 | 12 | from .. import tree 13 | 14 | 15 | class BadRequestsUseLinter(bad_kwarg_use.BadKwargUseLinter): 16 | """This linter looks for use of the "verify=False" kwarg when using the 17 | "requests" module. SSL verification is good, use SSL verification. 18 | 19 | http://docs.python-requests.org/en/master/user/advanced/#ssl-cert-verification 20 | """ 21 | off_by_default = False 22 | 23 | _code = 'DUO123' 24 | _error_tmpl = 'DUO123 use of "verify=False" is insecure in "requests" module' 25 | 26 | @property 27 | def kwargs(self): 28 | return [ 29 | { 30 | "module_path": "requests.request", 31 | "kwarg_name": "verify", 32 | "predicate": tree.kwarg_false, 33 | }, 34 | { 35 | "module_path": "requests.get", 36 | "kwarg_name": "verify", 37 | "predicate": tree.kwarg_false, 38 | }, 39 | { 40 | "module_path": "requests.options", 41 | "kwarg_name": "verify", 42 | "predicate": tree.kwarg_false, 43 | }, 44 | { 45 | "module_path": "requests.head", 46 | "kwarg_name": "verify", 47 | "predicate": tree.kwarg_false, 48 | }, 49 | { 50 | "module_path": "requests.post", 51 | "kwarg_name": "verify", 52 | "predicate": tree.kwarg_false, 53 | }, 54 | { 55 | "module_path": "requests.put", 56 | "kwarg_name": "verify", 57 | "predicate": tree.kwarg_false, 58 | }, 59 | { 60 | "module_path": "requests.patch", 61 | "kwarg_name": "verify", 62 | "predicate": tree.kwarg_false, 63 | }, 64 | { 65 | "module_path": "requests.delete", 66 | "kwarg_name": "verify", 67 | "predicate": tree.kwarg_false, 68 | }, 69 | ] 70 | -------------------------------------------------------------------------------- /tests/test_bad_zipfile_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadZipfileUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_zipfile_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import zipfile 21 | 22 | def func(): 23 | zf = zipfile.ZipFile() 24 | zf.extract() 25 | zf.extractall() 26 | """ 27 | ) 28 | 29 | linter = dlint.linters.BadZipfileUseLinter() 30 | linter.visit(python_node) 31 | 32 | result = linter.get_results() 33 | expected = [ 34 | dlint.linters.base.Flake8Result( 35 | lineno=6, 36 | col_offset=4, 37 | message=dlint.linters.BadZipfileUseLinter._error_tmpl 38 | ), 39 | dlint.linters.base.Flake8Result( 40 | lineno=7, 41 | col_offset=4, 42 | message=dlint.linters.BadZipfileUseLinter._error_tmpl 43 | ) 44 | ] 45 | 46 | assert result == expected 47 | 48 | def test_bad_zipfile_from_usage(self): 49 | python_node = self.get_ast_node( 50 | """ 51 | from zipfile import ZipFile 52 | 53 | def func(): 54 | zf = ZipFile() 55 | zf.extract() 56 | zf.extractall() 57 | """ 58 | ) 59 | 60 | linter = dlint.linters.BadZipfileUseLinter() 61 | linter.visit(python_node) 62 | 63 | result = linter.get_results() 64 | expected = [ 65 | dlint.linters.base.Flake8Result( 66 | lineno=6, 67 | col_offset=4, 68 | message=dlint.linters.BadZipfileUseLinter._error_tmpl 69 | ), 70 | dlint.linters.base.Flake8Result( 71 | lineno=7, 72 | col_offset=4, 73 | message=dlint.linters.BadZipfileUseLinter._error_tmpl 74 | ) 75 | ] 76 | 77 | assert result == expected 78 | 79 | 80 | if __name__ == "__main__": 81 | unittest.main() 82 | -------------------------------------------------------------------------------- /docs/linters/DUO116.md: -------------------------------------------------------------------------------- 1 | # DUO116 2 | 3 | This linter searches for use of the `subprocess` module with `shell=True`. 4 | This module commonly allows for arbitrary code execution bugs when combined 5 | with user input. 6 | 7 | ## Problematic code 8 | 9 | ```python 10 | import subprocess 11 | 12 | malicious_filename = "file; cat /etc/passwd" 13 | subprocess.Popen("touch " + malicious_filename, shell=True) 14 | ``` 15 | 16 | ## Correct code 17 | 18 | ```python 19 | import subprocess 20 | 21 | filename = "file" 22 | subprocess.Popen(["touch", filename]) 23 | ``` 24 | 25 | ## Rationale 26 | 27 | From the Python documentation: 28 | 29 | > Unlike some other popen functions, this implementation will never implicitly 30 | > call a system shell. This means that all characters, including shell 31 | > metacharacters, can safely be passed to child processes. If the shell is 32 | > invoked explicitly, `via shell=True`, it is the application's responsibility 33 | > to ensure that all whitespace and metacharacters are quoted appropriately to 34 | > avoid shell injection vulnerabilities. 35 | > 36 | > When using `shell=True`, the `shlex.quote()` function can be used to properly 37 | > escape whitespace and shell metacharacters in strings that are going to be 38 | > used to construct shell commands. 39 | 40 | ## Exceptions 41 | 42 | There are few situations where `shell=True` is actually necessary over simply 43 | passing a list of arguments to a subprocess. 44 | 45 | The Python documentation offers some insight into when it may be necessary, but 46 | also shows that you may not need shell-like features: 47 | 48 | > If `shell` is `True`, the specified command will be executed through the 49 | > shell. This can be useful if you are using Python primarily for the enhanced 50 | > control flow it offers over most system shells and still want convenient access 51 | > to other shell features such as shell pipes, filename wildcards, environment 52 | > variable expansion, and expansion of `~` to a user's home directory. However, 53 | > note that Python itself offers implementations of many shell-like features 54 | > (in particular, `glob`, `fnmatch`, `os.walk()`, `os.path.expandvars()`, 55 | > `os.path.expanduser()`, and `shutil`). 56 | 57 | Other exceptions include argument strings that have been correctly escaped, 58 | although correct escaping is a notoriously hard problem. 59 | -------------------------------------------------------------------------------- /docs/linters/DUO136.md: -------------------------------------------------------------------------------- 1 | # DUO136 2 | 3 | This linter searches for insecure attribute use of the `xmlsec` module. 4 | 5 | Cryptographic operations are notoriously difficult to get correct and often 6 | come with many gotchas. This linter searches for insecure cryptographic 7 | primitives and operations in the `xmlsec` library. 8 | 9 | ## Problematic code 10 | 11 | Any code using the following primitives/attributes should be considered 12 | cryptographically deprecated and insecure: 13 | 14 | ```python 15 | xmlsec.constants.TransformDes3Cbc 16 | xmlsec.constants.TransformKWDes3 17 | xmlsec.constants.TransformDsaSha1 18 | xmlsec.constants.TransformEcdsaSha1 19 | xmlsec.constants.TransformRsaMd5 20 | xmlsec.constants.TransformRsaRipemd160 21 | xmlsec.constants.TransformRsaSha1 22 | xmlsec.constants.TransformRsaPkcs1 23 | xmlsec.constants.TransformMd5 24 | xmlsec.constants.TransformRipemd160 25 | xmlsec.constants.TransformSha1 26 | ``` 27 | 28 | ## Correct code 29 | 30 | For `Des3` alternatives consider using AES, such as: 31 | `xmlsec.constants.TransformAes256Cbc`. 32 | 33 | For `Md5`, `Ripemd160`, and `Sha1` alternatives consider using the 34 | [SHA-2 family of hashing functions](https://en.wikipedia.org/wiki/SHA-2), 35 | such as: `xmlsec.constants.TransformRsaSha256`. 36 | 37 | For `Pkcs1` alternatives consider using OAEP: 38 | `xmlsec.constants.TransformRsaOaep`. 39 | 40 | ## Rationale 41 | 42 | The use of Triple DES is still considered secure is some scenarios, but it's 43 | usage should be updated to AES as soon as possible. Triple DES is vulnerable 44 | to the [Sweet32](https://sweet32.info/) attack in some scenarios. Further, 45 | Triple DES has been removed from the OpenSSL default cipher list in version 46 | 1.1.0 and has been removed from TLSv1.3, implying its usage should be limited 47 | moving forward. 48 | 49 | The problematic hashing algorithms mentioned above have known collision 50 | weaknesses. 51 | 52 | Finally, PKCS1, when used in encryption operations, is vulnerable to chosen 53 | ciphertext attacks. 54 | 55 | ## Exceptions 56 | 57 | * Triple DES may be used for legacy applications which encrypt relatively small 58 | amounts of information, e.g. small data sets, not long-lived web protocols. 59 | * `PKCS1` may be used for legacy applications, but should not be considered 60 | for new applications. It is still recommended to move away from `PKCS1` 61 | usage as soon as possible. 62 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | `Dlint` welcomes contributions from anyone. If you have an idea for a linter 4 | but don't know how to implement one please [create a new issue](https://github.com/dlint-py/dlint/issues). 5 | With `dlint` we can find security bugs, encourage best practices, and eliminate 6 | anti-patterns across the Python ecosystem. 7 | 8 | `Dlint` is built on top of Python's [AST](https://docs.python.org/3/library/ast.html) 9 | module and the [`flake8` plugin system](http://flake8.pycqa.org/en/latest/user/using-plugins.html). 10 | It may be helpful to review those systems before beginning `dlint` development, 11 | but `dlint` aims to be easily extendable without requiring a lot of background 12 | knowledge. **Further, please check out our brief section on [developing](https://github.com/dlint-py/dlint#developing) 13 | `dlint` before making changes.** 14 | 15 | # New Linters 16 | 17 | When adding new linters: 18 | 19 | * New linters should be added to the `dlint/linters/` directory. 20 | * Add a new file and class inheriting from `base.BaseLinter` for each new linter. 21 | * Add a "pass-through" import of the new class to `dlint.linters.__init__.py`. 22 | * Add the new class to `ALL` in `dlint.linters.__init__.py`. 23 | * Add documentation link in `docs/README.md`. 24 | * Add documentation file in `docs/linters/`. 25 | * Ensure new rules are properly tested (high or complete test coverage). 26 | * Ensure new code adheres to the style guide/linting process. 27 | * Add new rule information to `CHANGELOG.md` under `Unreleased` section, `Added` sub-section. 28 | 29 | From here, please create a [pull request](https://github.com/dlint-py/dlint/pulls) 30 | with your changes and wait for a review. 31 | 32 | # Fixing/Reporting Bugs 33 | 34 | When fixing or reporting bugs in `dlint` please [create a new issue](https://github.com/dlint-py/dlint/issues) 35 | first. This issue should include a snippet of code for reproducing the bug. 36 | 37 | E.g. 38 | 39 | *I expected `dlint` to flag the following code for faulty use of the `foo` module:* 40 | 41 | ``` 42 | from bar import foo 43 | 44 | var = result + 7 45 | widget = foo.baz(var) 46 | send_result(widget) 47 | ``` 48 | 49 | *Please update `dlint` to catch this. Thanks!* 50 | 51 | After reporting the issue, if you'd like to help fix it, please create a 52 | [pull request](https://github.com/dlint-py/dlint/pulls) with the 53 | fix applied and wait for a review. 54 | -------------------------------------------------------------------------------- /tests/test_bad_os_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadOSUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_os_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import os 21 | 22 | var = 'echo "TEST"' 23 | 24 | os.popen('ls') 25 | os.popen2('ls') 26 | os.popen3('ls') 27 | os.popen4('ls') 28 | os.system(var) 29 | os.tempnam() 30 | os.tmpnam() 31 | """ 32 | ) 33 | 34 | linter = dlint.linters.BadOSUseLinter() 35 | linter.visit(python_node) 36 | 37 | result = linter.get_results() 38 | expected = [ 39 | dlint.linters.base.Flake8Result( 40 | lineno=6, 41 | col_offset=0, 42 | message=dlint.linters.BadOSUseLinter._error_tmpl 43 | ), 44 | dlint.linters.base.Flake8Result( 45 | lineno=7, 46 | col_offset=0, 47 | message=dlint.linters.BadOSUseLinter._error_tmpl 48 | ), 49 | dlint.linters.base.Flake8Result( 50 | lineno=8, 51 | col_offset=0, 52 | message=dlint.linters.BadOSUseLinter._error_tmpl 53 | ), 54 | dlint.linters.base.Flake8Result( 55 | lineno=9, 56 | col_offset=0, 57 | message=dlint.linters.BadOSUseLinter._error_tmpl 58 | ), 59 | dlint.linters.base.Flake8Result( 60 | lineno=10, 61 | col_offset=0, 62 | message=dlint.linters.BadOSUseLinter._error_tmpl 63 | ), 64 | dlint.linters.base.Flake8Result( 65 | lineno=11, 66 | col_offset=0, 67 | message=dlint.linters.BadOSUseLinter._error_tmpl 68 | ), 69 | dlint.linters.base.Flake8Result( 70 | lineno=12, 71 | col_offset=0, 72 | message=dlint.linters.BadOSUseLinter._error_tmpl 73 | ), 74 | ] 75 | 76 | assert result == expected 77 | 78 | 79 | if __name__ == "__main__": 80 | unittest.main() 81 | -------------------------------------------------------------------------------- /dlint/linters/bad_duo_client_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | from .helpers import bad_kwarg_use 11 | 12 | from .. import tree 13 | 14 | 15 | class BadDuoClientUseLinter(bad_kwarg_use.BadKwargUseLinter): 16 | """This linter looks for unsafe HTTP use when using the "duo_client" module. 17 | """ 18 | off_by_default = False 19 | 20 | _code = 'DUO127' 21 | _error_tmpl = 'DUO127 use of "ca_certs=HTTP|DISABLE" is insecure in "duo_client" module' 22 | 23 | @property 24 | def kwargs(self): 25 | def http_or_disable(call, kwarg_name): 26 | return ( 27 | tree.kwarg_str(call, kwarg_name, "HTTP") 28 | or tree.kwarg_str(call, kwarg_name, "DISABLE") 29 | ) 30 | 31 | return [ 32 | { 33 | "module_path": "duo_client.Client", 34 | "kwarg_name": "ca_certs", 35 | "predicate": http_or_disable, 36 | }, 37 | { 38 | "module_path": "duo_client.AsyncDuoClient", 39 | "kwarg_name": "ca_certs", 40 | "predicate": http_or_disable, 41 | }, 42 | { 43 | "module_path": "duo_client.Auth", 44 | "kwarg_name": "ca_certs", 45 | "predicate": http_or_disable, 46 | }, 47 | { 48 | "module_path": "duo_client.AuthAPI", 49 | "kwarg_name": "ca_certs", 50 | "predicate": http_or_disable, 51 | }, 52 | { 53 | "module_path": "duo_client.Admin", 54 | "kwarg_name": "ca_certs", 55 | "predicate": http_or_disable, 56 | }, 57 | { 58 | "module_path": "duo_client.AdminAPI", 59 | "kwarg_name": "ca_certs", 60 | "predicate": http_or_disable, 61 | }, 62 | { 63 | "module_path": "duo_client.Accounts", 64 | "kwarg_name": "ca_certs", 65 | "predicate": http_or_disable, 66 | }, 67 | { 68 | "module_path": "duo_client.AccountsAPI", 69 | "kwarg_name": "ca_certs", 70 | "predicate": http_or_disable, 71 | }, 72 | ] 73 | -------------------------------------------------------------------------------- /docs/linters/DUO122.md: -------------------------------------------------------------------------------- 1 | # DUO122 2 | 3 | This linter looks for unsafe use of the Python `ssl` module. 4 | 5 | Making secure HTTPS connections is a notoriously difficult task filled with 6 | many non-obvious gotchas. This linter performs basic sanity checks on various 7 | common mistakes. 8 | 9 | ## Problematic code 10 | 11 | ```python 12 | import ssl 13 | 14 | ssl._https_verify_certificates(enable=False) 15 | ``` 16 | 17 | ```python 18 | import ssl 19 | import urllib 20 | 21 | context = ssl._create_unverified_context() 22 | urllib.urlopen("https://insecure-website", context=context) 23 | ``` 24 | 25 | ```python 26 | import ssl 27 | import urllib 28 | 29 | context = ssl.SSLContext(ssl.PROTOCOL_SSLv3) 30 | urllib.urlopen("https://insecure-website", context=context) 31 | ``` 32 | 33 | ```python 34 | import ssl 35 | import urllib 36 | 37 | context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) 38 | context.verify_mode = ssl.CERT_NONE 39 | urllib.urlopen("https://insecure-website", context=context) 40 | ``` 41 | 42 | ## Correct code 43 | 44 | ```python 45 | import ssl 46 | import urllib 47 | 48 | context = ssl.create_default_context() 49 | urllib.urlopen("https://secure-website", context=context) 50 | ``` 51 | 52 | ## Rationale 53 | 54 | For more information see [`ssl` security considerations](https://docs.python.org/3/library/ssl.html#ssl-security). 55 | 56 | ## Exceptions 57 | 58 | * `PROTOCOL_TLS` and `PROTOCOL_SSLv23` negotiate the highest protocol version 59 | both the client and server support. This means that the secure 60 | `PROTOCOL_TLSv1_2` may be chosen, or the insecure `PROTOCOL_SSLv3` may be 61 | chosen. This negotiation is prone to [downgrade attacks](https://en.wikipedia.org/wiki/Downgrade_attack), 62 | and leaves room for insecure connections, so we should prefer explicitly 63 | allowing only secure protocols. These attributes may be a false positive under 64 | certain circumstances, however we should err on the side of security. 65 | * There will often be times when using the most secure options will not be 66 | possible. For example, the server you're communicating with does not support 67 | modern protocols and is not under your control. Further, there can be network 68 | middleware (proxies, etc) that does not support the most secure options. In 69 | these situations the challenges may not be completely technical ones and you 70 | may have to fallback on insecure (or at least not *the most* secure) options - 71 | here you'll have to use your best judgement. 72 | * Connections to local development services or in test environments. 73 | -------------------------------------------------------------------------------- /dlint/linters/bad_urllib3_kwarg_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import functools 11 | 12 | from .helpers import bad_kwarg_use 13 | 14 | from .. import tree 15 | 16 | 17 | class BadUrllib3KwargUseLinter(bad_kwarg_use.BadKwargUseLinter): 18 | """This linter looks for unsafe use of urllib3 keyword arguments. These 19 | keyword arguments may indicate insecure connections are being performed. 20 | """ 21 | off_by_default = False 22 | 23 | _code = 'DUO132' 24 | _error_tmpl = 'DUO132 "urllib3" certificate verification disabled, insecure connections possible' 25 | 26 | @property 27 | def kwargs(self): 28 | # See 'urllib3.util.ssl_.resolve_cert_reqs' for more information 29 | def unverified_cert_reqs(call, kwarg_name): 30 | return tree.kwarg_any([ 31 | functools.partial( 32 | tree.kwarg_str, 33 | call, 34 | kwarg_name, 35 | "CERT_NONE" 36 | ), 37 | functools.partial( 38 | tree.kwarg_str, 39 | call, 40 | kwarg_name, 41 | "NONE" 42 | ), 43 | functools.partial( 44 | tree.kwarg_module_path, 45 | call, 46 | kwarg_name, 47 | "ssl.CERT_NONE", 48 | self.namespace 49 | ), 50 | ]) 51 | 52 | return [ 53 | { 54 | "module_path": "urllib3.PoolManager", 55 | "kwarg_name": "cert_reqs", 56 | "predicate": unverified_cert_reqs, 57 | }, 58 | { 59 | "module_path": "urllib3.ProxyManager", 60 | "kwarg_name": "cert_reqs", 61 | "predicate": unverified_cert_reqs, 62 | }, 63 | { 64 | "module_path": "urllib3.HTTPSConnectionPool", 65 | "kwarg_name": "cert_reqs", 66 | "predicate": unverified_cert_reqs, 67 | }, 68 | { 69 | "module_path": "urllib3.connection_from_url", 70 | "kwarg_name": "cert_reqs", 71 | "predicate": unverified_cert_reqs, 72 | }, 73 | { 74 | "module_path": "urllib3.proxy_from_url", 75 | "kwarg_name": "cert_reqs", 76 | "predicate": unverified_cert_reqs, 77 | }, 78 | ] 79 | -------------------------------------------------------------------------------- /dlint/linters/bad_re_catastrophic_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import ast 11 | 12 | from .helpers import bad_module_attribute_use 13 | from . import base 14 | from .. import redos 15 | 16 | 17 | class BadReCatastrophicUseLinter(bad_module_attribute_use.BadModuleAttributeUseLinter): 18 | """This linter looks for regular expression catastrophic backtracking in 19 | the re module. Catastrophic backtracking can cause denial-of-service. 20 | 21 | Some people, when confronted with a problem, think 22 | "I know, I'll use regular expressions." Now they have two problems. 23 | * Jamie Zawinski, 1997: http://regex.info/blog/2006-09-15/247 24 | """ 25 | off_by_default = False 26 | 27 | _code = 'DUO138' 28 | _error_tmpl = 'DUO138 catastrophic "re" usage - denial-of-service possible' 29 | 30 | @property 31 | def illegal_module_attributes(self): 32 | return { 33 | 're': [ 34 | 'compile', 35 | 'search', 36 | 'match', 37 | 'fullmatch', 38 | 'split', 39 | 'findall', 40 | 'finditer', 41 | 'sub', 42 | 'subn', 43 | ], 44 | 'django.core.validators': [ 45 | 'RegexValidator', 46 | ], 47 | 'django.urls': [ 48 | 're_path', 49 | ] 50 | } 51 | 52 | def __init__(self, *args, **kwargs): 53 | self.calls = {} 54 | 55 | super(BadReCatastrophicUseLinter, self).__init__(*args, **kwargs) 56 | 57 | def visit_Call(self, node): 58 | self.generic_visit(node) 59 | 60 | self.calls[node.func] = node 61 | 62 | def get_results(self): 63 | pattern_argument_number = 0 64 | 65 | def pattern_is_catastrophic(node): 66 | call = self.calls.get(node) 67 | if call is None or not call.args: 68 | return False 69 | 70 | pattern = call.args[pattern_argument_number] 71 | 72 | # Only handle string literals for now 73 | if not isinstance(pattern, ast.Str): 74 | return False 75 | 76 | return redos.detect.catastrophic(pattern.s) 77 | 78 | return [ 79 | base.Flake8Result( 80 | lineno=node.lineno, 81 | col_offset=node.col_offset, 82 | message=self._error_tmpl 83 | ) 84 | for node in self.bad_nodes 85 | if pattern_is_catastrophic(node) 86 | ] 87 | -------------------------------------------------------------------------------- /tests/test_helpers/test_bad_builtin_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | def get_builtin_use_implementation(illegal_builtin): 16 | class Cls(dlint.linters.helpers.bad_builtin_use.BadBuiltinUseLinter): 17 | _code = 'DUOXXX' 18 | _error_tmpl = 'DUOXXX error message' 19 | 20 | @property 21 | def illegal_builtin(self): 22 | return illegal_builtin 23 | 24 | return Cls() 25 | 26 | 27 | class TestBadBuiltinUse(dlint.test.base.BaseTest): 28 | 29 | def test_empty(self): 30 | python_node = self.get_ast_node( 31 | """ 32 | """ 33 | ) 34 | 35 | linter = get_builtin_use_implementation('foo') 36 | linter.visit(python_node) 37 | 38 | result = linter.get_results() 39 | expected = [] 40 | 41 | assert result == expected 42 | 43 | def test_bad_builtin_usage(self): 44 | python_node = self.get_ast_node( 45 | """ 46 | var = 1 47 | 48 | result = foo('var + 1') 49 | """ 50 | ) 51 | 52 | linter = get_builtin_use_implementation('foo') 53 | linter.visit(python_node) 54 | 55 | result = linter.get_results() 56 | expected = [ 57 | dlint.linters.base.Flake8Result( 58 | lineno=4, 59 | col_offset=9, 60 | message=linter._error_tmpl 61 | ) 62 | ] 63 | 64 | assert result == expected 65 | 66 | def test_bad_builtin_overwritten(self): 67 | python_node = self.get_ast_node( 68 | """ 69 | from foo import bar 70 | 71 | result = bar() 72 | """ 73 | ) 74 | 75 | linter = get_builtin_use_implementation('bar') 76 | linter.visit(python_node) 77 | 78 | result = linter.get_results() 79 | expected = [] 80 | 81 | assert result == expected 82 | 83 | def test_no_builtin_usage(self): 84 | python_node = self.get_ast_node( 85 | """ 86 | import os 87 | 88 | var = 'test' 89 | 90 | os.path.join(var) 91 | """ 92 | ) 93 | 94 | linter = get_builtin_use_implementation('foo') 95 | linter.visit(python_node) 96 | 97 | result = linter.get_results() 98 | expected = [] 99 | 100 | assert result == expected 101 | 102 | 103 | if __name__ == "__main__": 104 | unittest.main() 105 | -------------------------------------------------------------------------------- /dlint/linters/helpers/bad_module_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import abc 11 | 12 | from .. import base 13 | from ... import tree 14 | from ... import util 15 | 16 | 17 | class BadModuleUseLinter(base.BaseLinter, util.ABC): 18 | """This abstract base class provides an simple interface for creating new 19 | lint rules that block bad modules. 20 | """ 21 | 22 | @property 23 | @abc.abstractmethod 24 | def illegal_modules(self): 25 | """Subclasses must implement this property to return a list that 26 | looks like: 27 | 28 | [ 29 | "module_name1", 30 | "parent_module_name.module_name2", 31 | ] 32 | """ 33 | 34 | @property 35 | def whitelisted_modules(self): 36 | """Subclasses may implement this property to return a list that 37 | looks like: 38 | 39 | [ 40 | "parent_module_name.whitelisted_name1", 41 | ] 42 | """ 43 | return [] 44 | 45 | def visit_Import(self, node): 46 | import_names = [ 47 | alias.name for alias in node.names 48 | if alias.name not in self.whitelisted_modules 49 | ] 50 | 51 | bad_import = any( 52 | tree.same_modules(illegal_module, name) 53 | for illegal_module in self.illegal_modules 54 | for name in import_names 55 | ) 56 | 57 | if bad_import: 58 | self.results.append( 59 | base.Flake8Result( 60 | lineno=node.lineno, 61 | col_offset=node.col_offset, 62 | message=self._error_tmpl 63 | ) 64 | ) 65 | 66 | def visit_ImportFrom(self, node): 67 | if not node.module: 68 | # Relative imports, e.g. 'from .' or 'from ..' 69 | return 70 | 71 | from_import_names = [ 72 | node.module + "." + alias.name 73 | for alias in node.names 74 | if node.module + "." + alias.name not in self.whitelisted_modules 75 | ] 76 | 77 | bad_from_import = any( 78 | tree.same_modules(illegal_module, name) 79 | for illegal_module in self.illegal_modules 80 | for name in from_import_names 81 | ) 82 | 83 | if bad_from_import: 84 | self.results.append( 85 | base.Flake8Result( 86 | lineno=node.lineno, 87 | col_offset=node.col_offset, 88 | message=self._error_tmpl 89 | ) 90 | ) 91 | -------------------------------------------------------------------------------- /tests/test_bad_requests_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadRequestsUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_requests_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import requests 21 | 22 | requests.request(verify=False) 23 | requests.get(verify=False) 24 | requests.options(verify=False) 25 | requests.head(verify=False) 26 | requests.post(verify=False) 27 | requests.put(verify=False) 28 | requests.patch(verify=False) 29 | requests.delete(verify=False) 30 | """ 31 | ) 32 | 33 | linter = dlint.linters.BadRequestsUseLinter() 34 | linter.visit(python_node) 35 | 36 | result = linter.get_results() 37 | expected = [ 38 | dlint.linters.base.Flake8Result( 39 | lineno=4, 40 | col_offset=0, 41 | message=dlint.linters.BadRequestsUseLinter._error_tmpl 42 | ), 43 | dlint.linters.base.Flake8Result( 44 | lineno=5, 45 | col_offset=0, 46 | message=dlint.linters.BadRequestsUseLinter._error_tmpl 47 | ), 48 | dlint.linters.base.Flake8Result( 49 | lineno=6, 50 | col_offset=0, 51 | message=dlint.linters.BadRequestsUseLinter._error_tmpl 52 | ), 53 | dlint.linters.base.Flake8Result( 54 | lineno=7, 55 | col_offset=0, 56 | message=dlint.linters.BadRequestsUseLinter._error_tmpl 57 | ), 58 | dlint.linters.base.Flake8Result( 59 | lineno=8, 60 | col_offset=0, 61 | message=dlint.linters.BadRequestsUseLinter._error_tmpl 62 | ), 63 | dlint.linters.base.Flake8Result( 64 | lineno=9, 65 | col_offset=0, 66 | message=dlint.linters.BadRequestsUseLinter._error_tmpl 67 | ), 68 | dlint.linters.base.Flake8Result( 69 | lineno=10, 70 | col_offset=0, 71 | message=dlint.linters.BadRequestsUseLinter._error_tmpl 72 | ), 73 | dlint.linters.base.Flake8Result( 74 | lineno=11, 75 | col_offset=0, 76 | message=dlint.linters.BadRequestsUseLinter._error_tmpl 77 | ), 78 | ] 79 | 80 | assert result == expected 81 | 82 | 83 | if __name__ == "__main__": 84 | unittest.main() 85 | -------------------------------------------------------------------------------- /tests/test_bad_ssl_module_attribute_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadSSLModuleAttributeUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_ssl_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import ssl 21 | 22 | ssl._create_unverified_context() 23 | ssl._https_verify_certificates() 24 | ssl.CERT_NONE 25 | ssl.CERT_OPTIONAL 26 | ssl.PROTOCOL_SSLv2 27 | ssl.PROTOCOL_SSLv23 28 | ssl.PROTOCOL_SSLv3 29 | ssl.PROTOCOL_TLS 30 | """ 31 | ) 32 | 33 | linter = dlint.linters.BadSSLModuleAttributeUseLinter() 34 | linter.visit(python_node) 35 | 36 | result = linter.get_results() 37 | expected = [ 38 | dlint.linters.base.Flake8Result( 39 | lineno=4, 40 | col_offset=0, 41 | message=dlint.linters.BadSSLModuleAttributeUseLinter._error_tmpl 42 | ), 43 | dlint.linters.base.Flake8Result( 44 | lineno=5, 45 | col_offset=0, 46 | message=dlint.linters.BadSSLModuleAttributeUseLinter._error_tmpl 47 | ), 48 | dlint.linters.base.Flake8Result( 49 | lineno=6, 50 | col_offset=0, 51 | message=dlint.linters.BadSSLModuleAttributeUseLinter._error_tmpl 52 | ), 53 | dlint.linters.base.Flake8Result( 54 | lineno=7, 55 | col_offset=0, 56 | message=dlint.linters.BadSSLModuleAttributeUseLinter._error_tmpl 57 | ), 58 | dlint.linters.base.Flake8Result( 59 | lineno=8, 60 | col_offset=0, 61 | message=dlint.linters.BadSSLModuleAttributeUseLinter._error_tmpl 62 | ), 63 | dlint.linters.base.Flake8Result( 64 | lineno=9, 65 | col_offset=0, 66 | message=dlint.linters.BadSSLModuleAttributeUseLinter._error_tmpl 67 | ), 68 | dlint.linters.base.Flake8Result( 69 | lineno=10, 70 | col_offset=0, 71 | message=dlint.linters.BadSSLModuleAttributeUseLinter._error_tmpl 72 | ), 73 | dlint.linters.base.Flake8Result( 74 | lineno=11, 75 | col_offset=0, 76 | message=dlint.linters.BadSSLModuleAttributeUseLinter._error_tmpl 77 | ), 78 | ] 79 | 80 | assert result == expected 81 | 82 | 83 | if __name__ == "__main__": 84 | unittest.main() 85 | -------------------------------------------------------------------------------- /docs/linters/DUO133.md: -------------------------------------------------------------------------------- 1 | # DUO133 2 | 3 | This linter searches for use of the `Crypto` module. 4 | 5 | The `Crypto` module is provided by the [`pycrypto`](https://github.com/dlitz/pycrypto) 6 | library. This library is no longer maintained and has known vulnerabilities 7 | and exploits ([dlitz/pycrypto#176](https://github.com/dlitz/pycrypto/issues/176), 8 | [dlitz/pycrypto#253](https://github.com/dlitz/pycrypto/issues/253), 9 | [dlitz/pycrypto#269](https://github.com/dlitz/pycrypto/issues/269)). 10 | 11 | ## Problematic code 12 | 13 | ```python 14 | from Crypto.Cipher import AES 15 | 16 | obj = AES.new('This is a key', AES.MODE_CBC, 'This is an IV') 17 | message = 'One if by land, two if by sea' 18 | ciphertext = obj.encrypt(message) 19 | ``` 20 | 21 | ## Correct code 22 | 23 | This is just an example, but if you really are trying to accomplish symmetric 24 | key encryption then take a look at [Fernet](https://cryptography.io/en/latest/fernet/): 25 | 26 | ```python 27 | from cryptography.fernet import Fernet 28 | 29 | key = Fernet.generate_key() 30 | fernet = Fernet(key) 31 | message = b'One if by land, two if by sea' 32 | token = fernet.encrypt(message) 33 | ``` 34 | 35 | To illustrate corrections to the above code: 36 | 37 | ```python 38 | from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes 39 | from cryptography.hazmat.backends import default_backend 40 | 41 | cipher = Cipher( 42 | algorithms.AES('This is a key'), 43 | modes.CBC('This is an IV'), 44 | backend=default_backend() 45 | ) 46 | encryptor = cipher.encryptor() 47 | message = b'One if by land, two if by sea' 48 | ciphertext = encryptor.update(message) + encryptor.finalize() 49 | ``` 50 | 51 | ```python 52 | from Cryptodome.Cipher import AES 53 | 54 | obj = AES.new('This is a key', AES.MODE_CBC, 'This is an IV') 55 | message = 'One if by land, two if by sea' 56 | ciphertext = obj.encrypt(message) 57 | ``` 58 | 59 | ## Rationale 60 | 61 | Using `pycrypto` is insecure for these reasons: 62 | 63 | * The library is unmaintained - future bugs will not be fixed. 64 | * There are known vulnerabilities along with working exploits. 65 | * The library's API does not encourage safe-by-default, simple, obvious code. 66 | Cryptography operations are notorious difficult, so working with a library 67 | that prioritizes simplicity and safety should be preferred. 68 | 69 | The `cryptography` library is considered best-practice in the Python community. 70 | The `pycryptodomex` library should only be used when API-compatibility is 71 | necessary and `cryptography` cannot be used. Note that `pycryptodomex` is 72 | recommended over `pycryptodome` so Dlint can efficiently detect which library 73 | is being used. Both `pycrypto` and `pycryptodome` use the `Crypto` module, 74 | whereas `pycryptodomex` uses `Cryptodome`. This makes usage easier to detect. 75 | 76 | ## Exceptions 77 | 78 | None 79 | -------------------------------------------------------------------------------- /dlint/linters/bad_itsdangerous_kwarg_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import functools 11 | 12 | from .helpers import bad_kwarg_use 13 | 14 | from .. import tree 15 | 16 | 17 | class BadItsDangerousKwargUseLinter(bad_kwarg_use.BadKwargUseLinter): 18 | """This linter looks for unsafe use of itsdangerous keyword arguments. These 19 | keyword arguments may indicate insecure signing is being performed. 20 | """ 21 | off_by_default = False 22 | 23 | _code = 'DUO137' 24 | _error_tmpl = 'DUO137 insecure "itsdangerous" use allowing empty signing' 25 | 26 | @property 27 | def kwargs(self): 28 | def none_algorithm_predicate(call, kwarg_name): 29 | return tree.kwarg_any([ 30 | functools.partial( 31 | tree.kwarg_module_path_call, 32 | call, 33 | kwarg_name, 34 | "itsdangerous.signer.NoneAlgorithm", 35 | self.namespace 36 | ), 37 | functools.partial( 38 | tree.kwarg_module_path_call, 39 | call, 40 | kwarg_name, 41 | "itsdangerous.NoneAlgorithm", 42 | self.namespace 43 | ), 44 | ]) 45 | 46 | def none_string_predicate(call, kwarg_name): 47 | return tree.kwarg_str(call, kwarg_name, "none") 48 | 49 | return [ 50 | { 51 | "module_path": "itsdangerous.signer.Signer", 52 | "kwarg_name": "algorithm", 53 | "predicate": none_algorithm_predicate, 54 | }, 55 | { 56 | "module_path": "itsdangerous.Signer", 57 | "kwarg_name": "algorithm", 58 | "predicate": none_algorithm_predicate, 59 | }, 60 | { 61 | "module_path": "itsdangerous.timed.TimestampSigner", 62 | "kwarg_name": "algorithm", 63 | "predicate": none_algorithm_predicate, 64 | }, 65 | { 66 | "module_path": "itsdangerous.TimestampSigner", 67 | "kwarg_name": "algorithm", 68 | "predicate": none_algorithm_predicate, 69 | }, 70 | { 71 | "module_path": "itsdangerous.jws.JSONWebSignatureSerializer", 72 | "kwarg_name": "algorithm_name", 73 | "predicate": none_string_predicate, 74 | }, 75 | { 76 | "module_path": "itsdangerous.JSONWebSignatureSerializer", 77 | "kwarg_name": "algorithm_name", 78 | "predicate": none_string_predicate, 79 | }, 80 | ] 81 | -------------------------------------------------------------------------------- /docs/linters/DUO134.md: -------------------------------------------------------------------------------- 1 | # DUO134 2 | 3 | This linter searches for insecure attribute use of the `cryptography` module. 4 | 5 | The Python `cryptography` library has become the defacto standard for 6 | crytographic operations. Cryptographic operations are notoriously difficult 7 | to get correct and often come with many gotchas. This linter searches for 8 | insecure cryptographic primitives and operations in the `cryptography` library. 9 | 10 | ## Problematic code 11 | 12 | Any code using the following primitives/attributes should be considered 13 | cryptographically deprecated and insecure: 14 | 15 | ```python 16 | cryptography.hazmat.primitives.hashes.MD5 17 | cryptography.hazmat.primitives.hashes.SHA1 18 | cryptography.hazmat.primitives.ciphers.modes.ECB 19 | cryptography.hazmat.primitives.ciphers.algorithms.Blowfish 20 | cryptography.hazmat.primitives.ciphers.algorithms.ARC4 21 | cryptography.hazmat.primitives.ciphers.algorithms.IDEA 22 | ``` 23 | 24 | Dlint also looks for PKCS1 v1.5 usage via the following attribute: 25 | 26 | ```python 27 | cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15 28 | ``` 29 | 30 | This primitive is not as strictly insecure as the above primitives, but its 31 | usage should still be limited. 32 | 33 | ## Correct code 34 | 35 | There are secure alternatives documented in the following locations: 36 | 37 | * [https://cryptography.io/en/latest/hazmat/primitives/cryptographic-hashes/](https://cryptography.io/en/latest/hazmat/primitives/cryptographic-hashes/) 38 | * [https://cryptography.io/en/latest/hazmat/primitives/symmetric-encryption/#algorithms](https://cryptography.io/en/latest/hazmat/primitives/symmetric-encryption/#algorithms) 39 | 40 | For PKCS1 v1.5 alternatives consider the following: 41 | 42 | * For signing operations: [`cryptography.hazmat.primitives.asymmetric.padding.PSS`](https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/#cryptography.hazmat.primitives.asymmetric.padding.PSS) 43 | * For encryption operations: [`cryptography.hazmat.primitives.asymmetric.padding.OAEP`](https://cryptography.io/en/latest/hazmat/primitives/asymmetric/rsa/#cryptography.hazmat.primitives.asymmetric.padding.OAEP) 44 | 45 | ## Rationale 46 | 47 | The problematic hashing algorithms mentioned above have known collision 48 | weaknesses. 49 | 50 | The use of the ECB cipher mode can leave significant patterns in the output, 51 | which can be used for cryptanalysis. 52 | 53 | The problematic cipher algorithms mentioned above are susceptible to attacks 54 | when using weak keys and can have serious weaknesses in their initial stream 55 | output. 56 | 57 | Finally, PKCS1 v1.5, when used in encryption operations, is vulnerable to chosen 58 | ciphertext attacks. 59 | 60 | ## Exceptions 61 | 62 | * `PKCS1v15` may be used for legacy applications, but should not be considered 63 | for new applications. It is still recommended to move away from `PKCS1v15` 64 | usage as soon as possible. 65 | -------------------------------------------------------------------------------- /tests/test_bad_input_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import sys 11 | import unittest 12 | 13 | import dlint 14 | 15 | IS_PYTHON_2 = sys.version_info < (3, 0) 16 | 17 | 18 | class TestBadInputUse(dlint.test.base.BaseTest): 19 | 20 | def test_empty(self): 21 | python_node = self.get_ast_node( 22 | """ 23 | """ 24 | ) 25 | 26 | linter = dlint.linters.BadInputUseLinter() 27 | linter.visit(python_node) 28 | 29 | result = linter.get_results() 30 | expected = [] 31 | 32 | assert result == expected 33 | 34 | def test_bad_input_usage(self): 35 | python_node = self.get_ast_node( 36 | """ 37 | var = 1 38 | 39 | result = input('var + 1') 40 | """ 41 | ) 42 | 43 | linter = dlint.linters.BadInputUseLinter() 44 | linter.visit(python_node) 45 | 46 | result = linter.get_results() 47 | expected = [] if not IS_PYTHON_2 else [ 48 | dlint.linters.base.Flake8Result( 49 | lineno=4, 50 | col_offset=9, 51 | message=dlint.linters.BadInputUseLinter._error_tmpl 52 | ) 53 | ] 54 | 55 | assert result == expected 56 | 57 | def test_six_moves_input_usage(self): 58 | python_node = self.get_ast_node( 59 | """ 60 | from six.moves import input 61 | 62 | var = 1 63 | 64 | result = input('var + 1') 65 | """ 66 | ) 67 | 68 | linter = dlint.linters.BadInputUseLinter() 69 | linter.visit(python_node) 70 | 71 | result = linter.get_results() 72 | expected = [] 73 | 74 | assert result == expected 75 | 76 | def test_no_input_usage(self): 77 | python_node = self.get_ast_node( 78 | """ 79 | import os 80 | 81 | var = 'test' 82 | 83 | os.path.join(var) 84 | """ 85 | ) 86 | 87 | linter = dlint.linters.BadInputUseLinter() 88 | linter.visit(python_node) 89 | 90 | result = linter.get_results() 91 | expected = [] 92 | 93 | assert result == expected 94 | 95 | def test_bad_input_variable_usage(self): 96 | python_node = self.get_ast_node( 97 | """ 98 | input = 1 99 | """ 100 | ) 101 | 102 | linter = dlint.linters.BadInputUseLinter() 103 | linter.visit(python_node) 104 | 105 | result = linter.get_results() 106 | expected = [] 107 | 108 | assert result == expected 109 | 110 | 111 | if __name__ == "__main__": 112 | unittest.main() 113 | -------------------------------------------------------------------------------- /dlint/linters/helpers/bad_module_attribute_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import abc 11 | 12 | from .. import base 13 | from ... import tree 14 | from ... import util 15 | 16 | 17 | class BadModuleAttributeUseLinter(base.BaseLinter, util.ABC): 18 | """This abstract base class provides an simple interface for creating new 19 | lint rules that block bad attributes within a module. 20 | """ 21 | 22 | @property 23 | @abc.abstractmethod 24 | def illegal_module_attributes(self): 25 | """Subclasses must implement this property to return a dictionary 26 | that looks like: 27 | 28 | { 29 | "module_name": [ 30 | "attribute_name1", 31 | "attribute_name2", 32 | ] 33 | } 34 | """ 35 | 36 | def __init__(self, *args, **kwargs): 37 | self.bad_nodes = [] 38 | 39 | super(BadModuleAttributeUseLinter, self).__init__(*args, **kwargs) 40 | 41 | def get_results(self): 42 | 43 | return [ 44 | base.Flake8Result( 45 | lineno=node.lineno, 46 | col_offset=node.col_offset, 47 | message=self._error_tmpl 48 | ) 49 | for node in self.bad_nodes 50 | ] 51 | 52 | def visit_Name(self, node): 53 | def illegal_import_with_name_resolution(name, attributes, illegal_module_path): 54 | if name in attributes: 55 | resolved_name = name 56 | else: 57 | resolved_name = self.namespace.asname_to_name(name) 58 | if resolved_name not in attributes: 59 | return False 60 | 61 | return self.namespace.illegal_module_imported( 62 | resolved_name, 63 | illegal_module_path + "." + resolved_name 64 | ) 65 | 66 | illegal_call_use = any( 67 | illegal_import_with_name_resolution( 68 | node.id, 69 | attributes, 70 | illegal_module_path 71 | ) 72 | for illegal_module_path, attributes in self.illegal_module_attributes.items() 73 | ) 74 | 75 | if illegal_call_use: 76 | self.bad_nodes.append(node) 77 | 78 | def visit_Attribute(self, node): 79 | self.generic_visit(node) 80 | 81 | module_path = tree.module_path_str(node.value) 82 | 83 | illegal_attribute_use = any( 84 | node.attr in attributes 85 | and self.namespace.illegal_module_imported(module_path, illegal_module_path) 86 | for illegal_module_path, attributes in self.illegal_module_attributes.items() 87 | ) 88 | 89 | if illegal_attribute_use: 90 | self.bad_nodes.append(node) 91 | -------------------------------------------------------------------------------- /tests/test_namespace.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import pytest 13 | 14 | import dlint 15 | 16 | 17 | class TestNamespace(dlint.test.base.BaseTest): 18 | 19 | def test_basic_import(self): 20 | python_node = self.get_ast_node( 21 | """ 22 | import foo 23 | """ 24 | ) 25 | 26 | namespace = dlint.namespace.Namespace.from_module_node(python_node) 27 | 28 | result = namespace.name_imported("foo") 29 | expected = True 30 | 31 | assert result == expected 32 | 33 | def test_basic_from_import(self): 34 | python_node = self.get_ast_node( 35 | """ 36 | from foo import bar 37 | """ 38 | ) 39 | 40 | namespace = dlint.namespace.Namespace.from_module_node(python_node) 41 | 42 | result = namespace.name_imported("bar") 43 | expected = True 44 | 45 | assert result == expected 46 | 47 | def test_basic_as_import(self): 48 | python_node = self.get_ast_node( 49 | """ 50 | import foo as bar 51 | """ 52 | ) 53 | 54 | namespace = dlint.namespace.Namespace.from_module_node(python_node) 55 | 56 | result = namespace.name_imported("bar") 57 | expected = True 58 | 59 | assert result == expected 60 | 61 | def test_basic_as_from_import(self): 62 | python_node = self.get_ast_node( 63 | """ 64 | from foo import bar as baz 65 | """ 66 | ) 67 | 68 | namespace = dlint.namespace.Namespace.from_module_node(python_node) 69 | 70 | result = namespace.name_imported("baz") 71 | expected = True 72 | 73 | assert result == expected 74 | 75 | def test_as_import_mismatch(self): 76 | python_node = self.get_ast_node( 77 | """ 78 | import foo as bar 79 | """ 80 | ) 81 | 82 | namespace = dlint.namespace.Namespace.from_module_node(python_node) 83 | 84 | result = namespace.name_imported("foo") 85 | expected = False 86 | 87 | assert result == expected 88 | 89 | def test_as_from_import_mismatch(self): 90 | python_node = self.get_ast_node( 91 | """ 92 | from foo import bar as baz 93 | """ 94 | ) 95 | 96 | namespace = dlint.namespace.Namespace.from_module_node(python_node) 97 | 98 | result = ( 99 | namespace.name_imported("foo") 100 | or namespace.name_imported("bar") 101 | ) 102 | expected = False 103 | 104 | assert result == expected 105 | 106 | def test_from_module_node_unknown_type(self): 107 | unknown_type = None 108 | 109 | with pytest.raises(TypeError): 110 | dlint.namespace.Namespace.from_module_node(unknown_type) 111 | 112 | 113 | if __name__ == "__main__": 114 | unittest.main() 115 | -------------------------------------------------------------------------------- /tests/test_bad_tarfile_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadTarfileUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_tarfile_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import tarfile 21 | 22 | def func(): 23 | tf1 = tarfile.TarFile() 24 | tf2 = tarfile.TarFile.open() 25 | tf1.extract() 26 | tf2.extractall() 27 | tf1.extractall() 28 | tf2.extract() 29 | """ 30 | ) 31 | 32 | linter = dlint.linters.BadTarfileUseLinter() 33 | linter.visit(python_node) 34 | 35 | result = linter.get_results() 36 | expected = [ 37 | dlint.linters.base.Flake8Result( 38 | lineno=7, 39 | col_offset=4, 40 | message=dlint.linters.BadTarfileUseLinter._error_tmpl 41 | ), 42 | dlint.linters.base.Flake8Result( 43 | lineno=8, 44 | col_offset=4, 45 | message=dlint.linters.BadTarfileUseLinter._error_tmpl 46 | ), 47 | dlint.linters.base.Flake8Result( 48 | lineno=9, 49 | col_offset=4, 50 | message=dlint.linters.BadTarfileUseLinter._error_tmpl 51 | ), 52 | dlint.linters.base.Flake8Result( 53 | lineno=10, 54 | col_offset=4, 55 | message=dlint.linters.BadTarfileUseLinter._error_tmpl 56 | ) 57 | ] 58 | 59 | assert result == expected 60 | 61 | def test_bad_tarfile_from_usage(self): 62 | python_node = self.get_ast_node( 63 | """ 64 | from tarfile import TarFile 65 | 66 | def func(): 67 | tf1 = TarFile() 68 | tf2 = TarFile.open() 69 | tf1.extract() 70 | tf2.extractall() 71 | tf1.extractall() 72 | tf2.extract() 73 | """ 74 | ) 75 | 76 | linter = dlint.linters.BadTarfileUseLinter() 77 | linter.visit(python_node) 78 | 79 | result = linter.get_results() 80 | expected = [ 81 | dlint.linters.base.Flake8Result( 82 | lineno=7, 83 | col_offset=4, 84 | message=dlint.linters.BadTarfileUseLinter._error_tmpl 85 | ), 86 | dlint.linters.base.Flake8Result( 87 | lineno=8, 88 | col_offset=4, 89 | message=dlint.linters.BadTarfileUseLinter._error_tmpl 90 | ), 91 | dlint.linters.base.Flake8Result( 92 | lineno=9, 93 | col_offset=4, 94 | message=dlint.linters.BadTarfileUseLinter._error_tmpl 95 | ), 96 | dlint.linters.base.Flake8Result( 97 | lineno=10, 98 | col_offset=4, 99 | message=dlint.linters.BadTarfileUseLinter._error_tmpl 100 | ) 101 | ] 102 | 103 | assert result == expected 104 | 105 | 106 | if __name__ == "__main__": 107 | unittest.main() 108 | -------------------------------------------------------------------------------- /dlint/extension.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import importlib 11 | import inspect 12 | import pkgutil 13 | import optparse 14 | import sys 15 | 16 | from flake8 import style_guide 17 | 18 | import dlint 19 | 20 | 21 | class Flake8Extension(object): 22 | name = dlint.__name__ 23 | version = dlint.__version__ 24 | options = None 25 | 26 | def __init__(self, tree, filename): 27 | self.tree = tree 28 | self.filename = filename 29 | 30 | @classmethod 31 | def add_options(cls, parser): 32 | try: 33 | parser.add_option( 34 | '--print-dlint-linters', 35 | action='store_true', 36 | help='Print Dlint linter information.', 37 | parse_from_config=False 38 | ) 39 | except optparse.OptionConflictError: 40 | # Occurs during development when flake8 detects the dlint package 41 | # twice: once because it's been installed in editable mode, and 42 | # once from the local filesystem directory. We can safely nop here 43 | # since the option(s) have already been added. 44 | pass 45 | 46 | @classmethod 47 | def parse_options(cls, options): 48 | if options.print_dlint_linters: 49 | code_prefix_len = 7 50 | linters = cls.get_linter_classes() 51 | output_lines = [ 52 | "{} {} {}".format(l._code, l.__name__, l._error_tmpl[code_prefix_len:]) 53 | for l in sorted(linters, key=lambda li: li._code) 54 | ] 55 | print("\n".join(output_lines)) 56 | EX_OK = 0 57 | sys.exit(EX_OK) 58 | 59 | cls.options = options 60 | 61 | @classmethod 62 | def get_plugin_linter_classes(cls): 63 | module_prefix = 'dlint_plugin_' 64 | class_prefix = 'Dlint' 65 | 66 | plugin_modules = [ 67 | importlib.import_module(name) 68 | for finder, name, ispkg in pkgutil.iter_modules() 69 | if name.startswith(module_prefix) 70 | ] 71 | plugin_classes = [ 72 | inner_cls 73 | for module in plugin_modules 74 | for name, inner_cls in inspect.getmembers(module, predicate=inspect.isclass) 75 | if name.startswith(class_prefix) 76 | ] 77 | 78 | return plugin_classes 79 | 80 | @classmethod 81 | def get_linter_classes(cls): 82 | linter_classes = dlint.linters.ALL + tuple(cls.get_plugin_linter_classes()) 83 | 84 | if cls.options: 85 | engine = style_guide.DecisionEngine(cls.options) 86 | selected = style_guide.Decision.Selected 87 | linter_classes = tuple( 88 | linter_class for linter_class in linter_classes 89 | if engine.decision_for(linter_class._code) is selected 90 | ) 91 | 92 | return linter_classes 93 | 94 | def run(self): 95 | linter_instances = [l() for l in self.get_linter_classes()] 96 | multi_visitor = dlint.multi.MultiNodeVisitor(linter_instances) 97 | multi_visitor.visit(self.tree) 98 | 99 | for linter_instance in linter_instances: 100 | for result in linter_instance.get_results(): 101 | yield ( 102 | result.lineno, 103 | result.col_offset, 104 | result.message, 105 | type(linter_instance) 106 | ) 107 | -------------------------------------------------------------------------------- /tests/test_bad_onelogin_module_attribute_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadOneLoginModuleAttributeUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_onelogin_module_attribute_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import onelogin.saml2.utils.OneLogin_Saml2_Constants 21 | 22 | onelogin.saml2.utils.OneLogin_Saml2_Constants.SHA1 23 | onelogin.saml2.utils.OneLogin_Saml2_Constants.RSA_SHA1 24 | onelogin.saml2.utils.OneLogin_Saml2_Constants.DSA_SHA1 25 | onelogin.saml2.utils.OneLogin_Saml2_Constants.TRIPLEDES_CBC 26 | """ 27 | ) 28 | 29 | linter = dlint.linters.BadOneLoginModuleAttributeUseLinter() 30 | linter.visit(python_node) 31 | 32 | result = linter.get_results() 33 | expected = [ 34 | dlint.linters.base.Flake8Result( 35 | lineno=4, 36 | col_offset=0, 37 | message=dlint.linters.BadOneLoginModuleAttributeUseLinter._error_tmpl 38 | ), 39 | dlint.linters.base.Flake8Result( 40 | lineno=5, 41 | col_offset=0, 42 | message=dlint.linters.BadOneLoginModuleAttributeUseLinter._error_tmpl 43 | ), 44 | dlint.linters.base.Flake8Result( 45 | lineno=6, 46 | col_offset=0, 47 | message=dlint.linters.BadOneLoginModuleAttributeUseLinter._error_tmpl 48 | ), 49 | dlint.linters.base.Flake8Result( 50 | lineno=7, 51 | col_offset=0, 52 | message=dlint.linters.BadOneLoginModuleAttributeUseLinter._error_tmpl 53 | ), 54 | ] 55 | 56 | assert result == expected 57 | 58 | def test_bad_onelogin_module_attribute_usage_from_import(self): 59 | python_node = self.get_ast_node( 60 | """ 61 | from onelogin.saml2.utils import OneLogin_Saml2_Constants 62 | 63 | OneLogin_Saml2_Constants.SHA1 64 | OneLogin_Saml2_Constants.RSA_SHA1 65 | OneLogin_Saml2_Constants.DSA_SHA1 66 | OneLogin_Saml2_Constants.TRIPLEDES_CBC 67 | """ 68 | ) 69 | 70 | linter = dlint.linters.BadOneLoginModuleAttributeUseLinter() 71 | linter.visit(python_node) 72 | 73 | result = linter.get_results() 74 | expected = [ 75 | dlint.linters.base.Flake8Result( 76 | lineno=4, 77 | col_offset=0, 78 | message=dlint.linters.BadOneLoginModuleAttributeUseLinter._error_tmpl 79 | ), 80 | dlint.linters.base.Flake8Result( 81 | lineno=5, 82 | col_offset=0, 83 | message=dlint.linters.BadOneLoginModuleAttributeUseLinter._error_tmpl 84 | ), 85 | dlint.linters.base.Flake8Result( 86 | lineno=6, 87 | col_offset=0, 88 | message=dlint.linters.BadOneLoginModuleAttributeUseLinter._error_tmpl 89 | ), 90 | dlint.linters.base.Flake8Result( 91 | lineno=7, 92 | col_offset=0, 93 | message=dlint.linters.BadOneLoginModuleAttributeUseLinter._error_tmpl 94 | ), 95 | ] 96 | 97 | assert result == expected 98 | 99 | 100 | if __name__ == "__main__": 101 | unittest.main() 102 | -------------------------------------------------------------------------------- /dlint/linters/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import ( 2 | absolute_import, 3 | division, 4 | print_function, 5 | unicode_literals, 6 | ) 7 | 8 | from . import base # noqa F401 9 | from . import helpers # noqa F401 10 | 11 | from .bad_commands_use import BadCommandsUseLinter 12 | from .bad_compile_use import BadCompileUseLinter 13 | from .bad_cryptography_module_attribute_use import BadCryptographyModuleAttributeUseLinter 14 | from .bad_defusedxml_use import BadDefusedxmlUseLinter 15 | from .bad_dl_use import BadDlUseLinter 16 | from .bad_duo_client_use import BadDuoClientUseLinter 17 | from .bad_gl_use import BadGlUseLinter 18 | from .bad_eval_use import BadEvalUseLinter 19 | from .bad_exec_use import BadExecUseLinter 20 | from .bad_hashlib_use import BadHashlibUseLinter 21 | from .bad_input_use import BadInputUseLinter 22 | from .bad_itsdangerous_kwarg_use import BadItsDangerousKwargUseLinter 23 | from .bad_marshal_use import BadMarshalUseLinter 24 | from .bad_onelogin_kwarg_use import BadOneLoginKwargUseLinter 25 | from .bad_onelogin_module_attribute_use import BadOneLoginModuleAttributeUseLinter 26 | from .bad_os_use import BadOSUseLinter 27 | from .bad_popen2_use import BadPopen2UseLinter 28 | from .bad_random_generator_use import BadRandomGeneratorUseLinter 29 | from .bad_re_catastrophic_use import BadReCatastrophicUseLinter 30 | from .bad_requests_use import BadRequestsUseLinter 31 | from .bad_shelve_use import BadShelveUseLinter 32 | from .bad_subprocess_use import BadSubprocessUseLinter 33 | from .bad_ssl_module_attribute_use import BadSSLModuleAttributeUseLinter 34 | from .bad_sys_use import BadSysUseLinter 35 | from .bad_tarfile_use import BadTarfileUseLinter 36 | from .bad_tempfile_use import BadTempfileUseLinter 37 | from .bad_urllib3_module_attribute_use import BadUrllib3ModuleAttributeUseLinter 38 | from .bad_urllib3_kwarg_use import BadUrllib3KwargUseLinter 39 | from .bad_pickle_use import BadPickleUseLinter 40 | from .bad_pycrypto_use import BadPycryptoUseLinter 41 | from .bad_xml_use import BadXMLUseLinter 42 | from .bad_xmlrpc_use import BadXmlrpcUseLinter 43 | from .bad_xmlsec_module_attribute_use import BadXmlsecModuleAttributeUseLinter 44 | from .bad_yaml_use import BadYAMLUseLinter 45 | from .bad_zipfile_use import BadZipfileUseLinter 46 | 47 | from .twisted.inlinecallbacks_yield_statement import InlineCallbacksYieldStatementLinter 48 | from .twisted.returnvalue_in_inlinecallbacks import ReturnValueInInlineCallbacksLinter 49 | from .twisted.yield_return_statement import YieldReturnStatementLinter 50 | 51 | ALL = ( 52 | BadCommandsUseLinter, 53 | BadCompileUseLinter, 54 | BadCryptographyModuleAttributeUseLinter, 55 | BadDefusedxmlUseLinter, 56 | BadDlUseLinter, 57 | BadDuoClientUseLinter, 58 | BadGlUseLinter, 59 | BadEvalUseLinter, 60 | BadExecUseLinter, 61 | BadHashlibUseLinter, 62 | BadInputUseLinter, 63 | BadItsDangerousKwargUseLinter, 64 | BadMarshalUseLinter, 65 | BadOneLoginKwargUseLinter, 66 | BadOneLoginModuleAttributeUseLinter, 67 | BadOSUseLinter, 68 | BadPopen2UseLinter, 69 | BadRandomGeneratorUseLinter, 70 | BadReCatastrophicUseLinter, 71 | BadRequestsUseLinter, 72 | BadShelveUseLinter, 73 | BadSSLModuleAttributeUseLinter, 74 | BadSysUseLinter, 75 | BadSubprocessUseLinter, 76 | BadTempfileUseLinter, 77 | BadTarfileUseLinter, 78 | BadUrllib3ModuleAttributeUseLinter, 79 | BadUrllib3KwargUseLinter, 80 | BadPickleUseLinter, 81 | BadPycryptoUseLinter, 82 | BadXMLUseLinter, 83 | BadXmlrpcUseLinter, 84 | BadXmlsecModuleAttributeUseLinter, 85 | BadYAMLUseLinter, 86 | BadZipfileUseLinter, 87 | InlineCallbacksYieldStatementLinter, 88 | ReturnValueInInlineCallbacksLinter, 89 | YieldReturnStatementLinter, 90 | ) 91 | -------------------------------------------------------------------------------- /tests/test_bad_defusedxml_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadDefusedxmlUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_defusedxml_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import defusedxml.lxml 21 | import defusedxml.ElementTree 22 | import defusedxml.cElementTree 23 | 24 | defusedxml.lxml.parse("") 25 | defusedxml.ElementTree.fromstring("", forbid_entities=False) 26 | defusedxml.cElementTree.iterparse("", forbid_external=False) 27 | """ 28 | ) 29 | 30 | linter = dlint.linters.BadDefusedxmlUseLinter() 31 | linter.visit(python_node) 32 | 33 | result = linter.get_results() 34 | expected = [ 35 | dlint.linters.base.Flake8Result( 36 | lineno=6, 37 | col_offset=0, 38 | message=dlint.linters.BadDefusedxmlUseLinter._error_tmpl 39 | ), 40 | dlint.linters.base.Flake8Result( 41 | lineno=7, 42 | col_offset=0, 43 | message=dlint.linters.BadDefusedxmlUseLinter._error_tmpl 44 | ), 45 | dlint.linters.base.Flake8Result( 46 | lineno=8, 47 | col_offset=0, 48 | message=dlint.linters.BadDefusedxmlUseLinter._error_tmpl 49 | ), 50 | ] 51 | 52 | assert result == expected 53 | 54 | def test_bad_defusedxml_from_usage(self): 55 | python_node = self.get_ast_node( 56 | """ 57 | from defusedxml.lxml import parse 58 | from defusedxml.ElementTree import fromstring 59 | from defusedxml.cElementTree import iterparse 60 | 61 | parse("") 62 | fromstring("", forbid_entities=False) 63 | iterparse("", forbid_external=False) 64 | """ 65 | ) 66 | 67 | linter = dlint.linters.BadDefusedxmlUseLinter() 68 | linter.visit(python_node) 69 | 70 | result = linter.get_results() 71 | expected = [ 72 | dlint.linters.base.Flake8Result( 73 | lineno=6, 74 | col_offset=0, 75 | message=dlint.linters.BadDefusedxmlUseLinter._error_tmpl 76 | ), 77 | dlint.linters.base.Flake8Result( 78 | lineno=7, 79 | col_offset=0, 80 | message=dlint.linters.BadDefusedxmlUseLinter._error_tmpl 81 | ), 82 | dlint.linters.base.Flake8Result( 83 | lineno=8, 84 | col_offset=0, 85 | message=dlint.linters.BadDefusedxmlUseLinter._error_tmpl 86 | ), 87 | ] 88 | 89 | assert result == expected 90 | 91 | def test_defusedxml_avoid_false_positive(self): 92 | python_node = self.get_ast_node( 93 | """ 94 | from otherlib1 import parse 95 | from otherlib2 import fromstring 96 | from otherlib3 import iterparse 97 | 98 | parse("") 99 | fromstring("", forbid_entities=False) 100 | iterparse("", forbid_external=False) 101 | """ 102 | ) 103 | 104 | linter = dlint.linters.BadDefusedxmlUseLinter() 105 | linter.visit(python_node) 106 | 107 | result = linter.get_results() 108 | expected = [] 109 | 110 | assert result == expected 111 | 112 | 113 | if __name__ == "__main__": 114 | unittest.main() 115 | -------------------------------------------------------------------------------- /docs/linters/DUO138.md: -------------------------------------------------------------------------------- 1 | # DUO138 2 | 3 | This linter searches for regular expressions that may, under certain inputs, 4 | exhibit catastrophic backtracking in the Python [`re`](https://docs.python.org/3/library/re.html) 5 | module. For more information on catastrophic backtracking see: 6 | 7 | * [Runaway Regular Expressions: Catastrophic Backtracking](https://www.regular-expressions.info/catastrophic.html) 8 | * [Preventing Regular Expression Denial of Service (ReDoS)](https://www.regular-expressions.info/redos.html) 9 | * [Regex Performance](https://blog.codinghorror.com/regex-performance/) 10 | * [Regular Expression Denial of Service (ReDoS) and Catastrophic Backtracking](https://snyk.io/blog/redos-and-catastrophic-backtracking/) 11 | * [Javascript Catastrophic Backtracking](https://javascript.info/regexp-catastrophic-backtracking) 12 | 13 | ## Problematic code 14 | 15 | There are many ways that ReDoS can occur. The following examples show ReDoS via 16 | nested quantifier and mutually inclusive alternation, respectively: 17 | 18 | ```python 19 | import re 20 | 21 | subject = 'x' * 64 22 | re.search(r'(x+x+)+y', subject) # Boom 23 | ``` 24 | 25 | ```python 26 | import re 27 | 28 | subject = 'a' * 64 29 | re.search(r'(.|[abc])+z', subject) # Boom 30 | ``` 31 | 32 | ## Correct code 33 | 34 | The above examples can be corrected with the following changes: 35 | 36 | ```python 37 | import re 38 | 39 | subject = 'x' * 64 40 | re.search(r'xx+y', subject) 41 | ``` 42 | 43 | ```python 44 | import re 45 | 46 | subject = 'a' * 64 47 | re.search(r'.+z', subject) 48 | ``` 49 | 50 | Rarely will there be one-size-fits-all solutions to catastrophic backtracking. 51 | Developing a deeper understanding of the issue, and regular expressions in 52 | general, is often the best solution. Whenenever you're working with regular 53 | expressions you must always ask yourself if this is a potentially catastrophic 54 | expression. 55 | 56 | ## Rationale 57 | 58 | Catastrophic backtracking will often lead to denial-of-service. Catastrophic 59 | cases may take days, weeks, or years to complete which may leave your service 60 | degraded or unusable. 61 | 62 | ## Exceptions 63 | 64 | * Nested quantifiers with small maximums may be okay (e.g. `{1,3}`). However, 65 | sensible runtimes for your code are application-dependent. Even with small 66 | maximums the runtime will depend on many factors including subject length, 67 | machine hardware, and repetition size. Proceed with nested quantifiers at your 68 | own risk. 69 | 70 | ## Debugging 71 | 72 | The Dlint ReDoS module (`dlint.redos`) comes with a CLI interface for quick 73 | debugging and testing. E.g. 74 | 75 | ``` 76 | $ python -m dlint.redos --pattern '(.|[abc])+z' 77 | ('(.|[abc])+z', True) 78 | ``` 79 | 80 | ``` 81 | $ python -m dlint.redos --pattern '.+z' 82 | ('.+z', False) 83 | ``` 84 | 85 | You can dump the pattern in a more digestible format: 86 | 87 | ``` 88 | $ python -m dlint.redos --pattern '(.|[abc])+z' --dump 89 | MAX_REPEAT 1 MAXREPEAT 90 | SUBPATTERN 1 0 0 91 | BRANCH 92 | ANY None 93 | OR 94 | IN 95 | LITERAL 97 96 | LITERAL 98 97 | LITERAL 99 98 | LITERAL 122 99 | ``` 100 | 101 | Or dump the internal tree representation that Dlint uses: 102 | 103 | ``` 104 | $ python -m dlint.redos --pattern '(.|[abc])+z' --dump-tree 105 | None: () 106 | MAX_REPEAT: (1, MAXREPEAT) 107 | SUBPATTERN: (1, 0, 0) 108 | BRANCH: () 109 | ANY: (None,) 110 | IN: ((LITERAL, 97), (LITERAL, 98), (LITERAL, 99)) 111 | LITERAL: (122,) 112 | ``` 113 | 114 | You can also extend this functionality to analyze patterns from other 115 | regular expression modules or languages by piping a pattern to this interface: 116 | 117 | ``` 118 | $ echo -n '(.|[abc])+z' | python -m dlint.redos --pattern - 119 | ('(.|[abc])+z', True) 120 | ``` 121 | -------------------------------------------------------------------------------- /tests/test_bad_xmlsec_module_attribute_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import unittest 11 | 12 | import dlint 13 | 14 | 15 | class TestBadXmlsecModuleAttributeUse(dlint.test.base.BaseTest): 16 | 17 | def test_bad_xmlsec_module_attribute_usage(self): 18 | python_node = self.get_ast_node( 19 | """ 20 | import xmlsec.constants 21 | 22 | xmlsec.constants.TransformDes3Cbc 23 | xmlsec.constants.TransformKWDes3 24 | xmlsec.constants.TransformDsaSha1 25 | xmlsec.constants.TransformEcdsaSha1 26 | xmlsec.constants.TransformRsaMd5 27 | xmlsec.constants.TransformRsaRipemd160 28 | xmlsec.constants.TransformRsaSha1 29 | xmlsec.constants.TransformRsaPkcs1 30 | xmlsec.constants.TransformMd5 31 | xmlsec.constants.TransformRipemd160 32 | xmlsec.constants.TransformSha1 33 | """ 34 | ) 35 | 36 | linter = dlint.linters.BadXmlsecModuleAttributeUseLinter() 37 | linter.visit(python_node) 38 | 39 | result = linter.get_results() 40 | expected = [ 41 | dlint.linters.base.Flake8Result( 42 | lineno=4, 43 | col_offset=0, 44 | message=dlint.linters.BadXmlsecModuleAttributeUseLinter._error_tmpl 45 | ), 46 | dlint.linters.base.Flake8Result( 47 | lineno=5, 48 | col_offset=0, 49 | message=dlint.linters.BadXmlsecModuleAttributeUseLinter._error_tmpl 50 | ), 51 | dlint.linters.base.Flake8Result( 52 | lineno=6, 53 | col_offset=0, 54 | message=dlint.linters.BadXmlsecModuleAttributeUseLinter._error_tmpl 55 | ), 56 | dlint.linters.base.Flake8Result( 57 | lineno=7, 58 | col_offset=0, 59 | message=dlint.linters.BadXmlsecModuleAttributeUseLinter._error_tmpl 60 | ), 61 | dlint.linters.base.Flake8Result( 62 | lineno=8, 63 | col_offset=0, 64 | message=dlint.linters.BadXmlsecModuleAttributeUseLinter._error_tmpl 65 | ), 66 | dlint.linters.base.Flake8Result( 67 | lineno=9, 68 | col_offset=0, 69 | message=dlint.linters.BadXmlsecModuleAttributeUseLinter._error_tmpl 70 | ), 71 | dlint.linters.base.Flake8Result( 72 | lineno=10, 73 | col_offset=0, 74 | message=dlint.linters.BadXmlsecModuleAttributeUseLinter._error_tmpl 75 | ), 76 | dlint.linters.base.Flake8Result( 77 | lineno=11, 78 | col_offset=0, 79 | message=dlint.linters.BadXmlsecModuleAttributeUseLinter._error_tmpl 80 | ), 81 | dlint.linters.base.Flake8Result( 82 | lineno=12, 83 | col_offset=0, 84 | message=dlint.linters.BadXmlsecModuleAttributeUseLinter._error_tmpl 85 | ), 86 | dlint.linters.base.Flake8Result( 87 | lineno=13, 88 | col_offset=0, 89 | message=dlint.linters.BadXmlsecModuleAttributeUseLinter._error_tmpl 90 | ), 91 | dlint.linters.base.Flake8Result( 92 | lineno=14, 93 | col_offset=0, 94 | message=dlint.linters.BadXmlsecModuleAttributeUseLinter._error_tmpl 95 | ), 96 | ] 97 | 98 | assert result == expected 99 | 100 | 101 | if __name__ == "__main__": 102 | unittest.main() 103 | -------------------------------------------------------------------------------- /dlint/linters/helpers/bad_kwarg_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import abc 11 | import ast 12 | import itertools 13 | 14 | from .. import base 15 | from ... import tree 16 | from ... import util 17 | 18 | 19 | class BadKwargUseLinter(base.BaseLinter, util.ABC): 20 | """This abstract base class provides an simple interface for creating new 21 | lint rules that block bad kwarg use. 22 | """ 23 | 24 | def __init__(self, *args, **kwargs): 25 | self.minimized_bad_kwarg_func = None 26 | 27 | module_path_grouped = [ 28 | (k, list(v)) 29 | for k, v in itertools.groupby( 30 | sorted(self.kwargs, key=lambda k: k["module_path"]), 31 | key=lambda k: k["module_path"] 32 | ) 33 | ] 34 | 35 | def minimized_illegal_module_imported(module_path, node): 36 | return any( 37 | self.namespace.illegal_module_imported( 38 | module_path, 39 | kwarg["module_path"] 40 | ) 41 | and kwarg["predicate"](node, kwarg["kwarg_name"]) 42 | for illegal_module_path, kwargs in module_path_grouped 43 | for kwarg in kwargs 44 | ) 45 | 46 | kwarg_predicate_grouped = [ 47 | (k, list(v)) 48 | for k, v in itertools.groupby( 49 | sorted(self.kwargs, key=lambda k: (k["kwarg_name"], id(k["predicate"]))), 50 | key=lambda k: (k["kwarg_name"], id(k["predicate"])) 51 | ) 52 | ] 53 | 54 | def minimized_kwarg_predicate(module_path, node): 55 | return any( 56 | self.namespace.illegal_module_imported( 57 | module_path, 58 | kwarg["module_path"] 59 | ) 60 | and kwarg["predicate"](node, kwarg["kwarg_name"]) 61 | for kwarg_predicate_tuple, kwargs in kwarg_predicate_grouped 62 | for kwarg in kwargs 63 | ) 64 | 65 | # Minimize kwarg checks by grouping similar rules 66 | if (len(kwarg_predicate_grouped) < len(self.kwargs) 67 | and len(module_path_grouped) == len(self.kwargs)): 68 | self.minimized_bad_kwarg_func = minimized_kwarg_predicate 69 | else: 70 | self.minimized_bad_kwarg_func = minimized_illegal_module_imported 71 | 72 | super(BadKwargUseLinter, self).__init__(*args, **kwargs) 73 | 74 | @property 75 | @abc.abstractmethod 76 | def kwargs(self): 77 | """Subclasses must implement this property to return a list that 78 | looks like: 79 | 80 | [ 81 | { 82 | "module_path": "mod1.mod2.name1", 83 | "kwarg_name": "kwarg1", 84 | "predicate": , 85 | }, 86 | ] 87 | 88 | Which would represent 'mod1.mod2.name1(kwarg1=...)' where 'predicate' 89 | is a function that takes the Call object and a kwarg name and returns 90 | True|False. 91 | """ 92 | 93 | def visit_Call(self, node): 94 | self.generic_visit(node) 95 | 96 | if not isinstance(node.func, (ast.Attribute, ast.Name)): 97 | return 98 | 99 | bad_kwarg = self.minimized_bad_kwarg_func( 100 | tree.module_path_str(node.func), 101 | node 102 | ) 103 | 104 | if bad_kwarg: 105 | self.results.append( 106 | base.Flake8Result( 107 | lineno=node.lineno, 108 | col_offset=node.col_offset, 109 | message=self._error_tmpl 110 | ) 111 | ) 112 | -------------------------------------------------------------------------------- /dlint/linters/helpers/bad_name_attribute_use.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import abc 11 | import ast 12 | import collections 13 | 14 | from .. import base 15 | from ... import tree 16 | from ... import util 17 | 18 | Assignment = collections.namedtuple( 19 | 'Assignment', 20 | ['variable', 'module_path', 'lineno', 'col_offset'] 21 | ) 22 | 23 | 24 | class BadNameAttributeUseLinter(base.BaseLinter, util.ABC): 25 | """This abstract base class provides an simple interface for creating new 26 | lint rules that block bad attributes on a variable object. 27 | """ 28 | 29 | @property 30 | @abc.abstractmethod 31 | def illegal_name_attributes(self): 32 | """Subclasses must implement this property to return a dictionary 33 | that looks like: 34 | 35 | { 36 | "object_attribute": [ 37 | "parent_module_name1.child_module_name1", 38 | "parent_module_name2.child_module_name2", 39 | ] 40 | } 41 | """ 42 | 43 | def visit_FunctionDef(self, node): 44 | self.generic_visit(node) 45 | 46 | targets = [] 47 | 48 | def variable_assignment_callback(inner_node): 49 | if not (isinstance(inner_node, ast.Assign) 50 | and isinstance(inner_node.value, ast.Call)): 51 | return 52 | 53 | module_path = tree.module_path_str(inner_node.value.func) 54 | targets.extend( 55 | Assignment( 56 | variable=target.id, 57 | module_path=module_path, 58 | lineno=inner_node.lineno, 59 | col_offset=inner_node.col_offset, 60 | ) 61 | for target in inner_node.targets 62 | if isinstance(target, ast.Name) 63 | ) 64 | 65 | tree.walk_callback_same_scope(node, variable_assignment_callback) 66 | 67 | results = [] 68 | 69 | def attribute_use_callback(inner_node): 70 | if not (isinstance(inner_node, ast.Call) 71 | and isinstance(inner_node.func, ast.Attribute) 72 | and isinstance(inner_node.func.value, ast.Name)): 73 | return 74 | 75 | variable = inner_node.func.value.id 76 | attribute = inner_node.func.attr 77 | 78 | illegal_calls = [ 79 | target for target in targets 80 | if target.variable == variable 81 | and attribute in self.illegal_name_attributes 82 | and any( 83 | self.namespace.illegal_module_imported( 84 | target.module_path, 85 | illegal_name 86 | ) 87 | for illegal_name in self.illegal_name_attributes[attribute] 88 | ) 89 | ] 90 | 91 | try: 92 | latest_variable_assignment = max( 93 | [ 94 | target for target in targets 95 | if target.variable == variable 96 | ], 97 | key=lambda target: (target.lineno, target.col_offset) 98 | ) 99 | except ValueError: 100 | # No variable name matches 101 | return 102 | 103 | if latest_variable_assignment in illegal_calls: 104 | results.append(inner_node) 105 | 106 | tree.walk_callback_same_scope(node, attribute_use_callback) 107 | 108 | self.results.extend( 109 | base.Flake8Result( 110 | lineno=result.lineno, 111 | col_offset=result.col_offset, 112 | message=self._error_tmpl 113 | ) 114 | for result in results 115 | ) 116 | 117 | def visit_AsyncFunctionDef(self, node): 118 | self.visit_FunctionDef(node) 119 | -------------------------------------------------------------------------------- /dlint/namespace.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import ( 4 | absolute_import, 5 | division, 6 | print_function, 7 | unicode_literals, 8 | ) 9 | 10 | import ast 11 | import copy 12 | 13 | try: 14 | from functools import lru_cache 15 | except ImportError: 16 | # Sorry Python 2 users, it's time to upgrade 17 | def lru_cache(*args, **kwargs): 18 | def decorator(function): 19 | def noop(*inner_args, **inner_kwargs): 20 | return function(*inner_args, **inner_kwargs) 21 | return noop 22 | return decorator 23 | 24 | from . import util 25 | 26 | 27 | class Namespace(object): 28 | def __init__(self, imports, from_imports): 29 | self.imports = imports 30 | self.from_imports = from_imports 31 | 32 | @classmethod 33 | def from_module_node(cls, module_node): 34 | # For now only add top-level, module imports. Let's avoid the rabbit 35 | # hole of looking at things like function and class-scope imports and 36 | # conditional imports in 'if' or 'try' statements 37 | if not isinstance(module_node, ast.Module): 38 | raise TypeError('expected type ast.Module, received {}'.format(type(module_node))) 39 | 40 | imports = [] 41 | from_imports = [] 42 | 43 | for node in module_node.body: 44 | if isinstance(node, ast.Import): 45 | imports.append(copy.copy(node)) 46 | elif isinstance(node, ast.ImportFrom): 47 | from_imports.append(copy.copy(node)) 48 | 49 | return cls(imports, from_imports) 50 | 51 | @lru_cache(maxsize=1024) 52 | def name_imported(self, name): 53 | def alias_includes_name(alias): 54 | return ( 55 | (alias.name == name and alias.asname is None) 56 | or (alias.asname == name) 57 | ) 58 | 59 | return any( 60 | alias_includes_name(alias) 61 | for imp in self.imports + self.from_imports 62 | for alias in imp.names 63 | ) 64 | 65 | def asname_to_name(self, asname): 66 | for imp in self.imports + self.from_imports: 67 | for alias in imp.names: 68 | if alias.asname == asname: 69 | return alias.name 70 | 71 | return None 72 | 73 | @lru_cache(maxsize=1024) 74 | def illegal_module_imported(self, module_path, illegal_module_path): 75 | modules = module_path.split('.') 76 | illegal_modules = illegal_module_path.split('.') 77 | 78 | module_imported = False 79 | canonicalized_modules = modules 80 | 81 | for imp in self.imports: 82 | for alias in imp.names: 83 | if util.lstartswith(illegal_modules, alias.name.split('.')): 84 | module_imported = True 85 | if modules[0] == alias.asname: 86 | # 'import foo.bar as baz', 'baz.qux' -> 'foo.bar.baz' 87 | canonicalized_modules = alias.name.split('.') + modules[1:] 88 | 89 | for imp in self.from_imports: 90 | if not imp.module: 91 | continue # Relative import, e.g. 'from .' 92 | 93 | imp_modules = imp.module.split('.') 94 | 95 | for alias in imp.names: 96 | if util.lstartswith(illegal_modules, imp_modules + [alias.name]): 97 | module_imported = True 98 | if modules[0] in [alias.name, alias.asname]: 99 | # 'from foo.bar import baz as qux', 'qux.quine' -> 'foo.bar.baz.quine' 100 | canonicalized_modules = imp_modules + [alias.name] + modules[1:] 101 | if (util.lstartswith(illegal_modules, imp_modules) 102 | and illegal_modules[len(imp_modules):] == modules 103 | and alias.name == '*'): 104 | # 'from foo.bar import *', 'baz.qux' -> 'foo.bar.baz.qux' 105 | module_imported = True 106 | canonicalized_modules = imp_modules + modules 107 | 108 | return module_imported and (canonicalized_modules == illegal_modules) 109 | --------------------------------------------------------------------------------