diff --git a/README.rst b/README.rst index 8ca8bf8bd..c9659998f 100644 --- a/README.rst +++ b/README.rst @@ -219,6 +219,7 @@ Usage:: B323 unverified_context B324 hashlib_new_insecure_functions B325 tempnam + B326 ast_overflow B401 import_telnetlib B402 import_ftplib B403 import_pickle diff --git a/bandit/blacklists/calls.py b/bandit/blacklists/calls.py index 85bec160e..ab1236787 100644 --- a/bandit/blacklists/calls.py +++ b/bandit/blacklists/calls.py @@ -311,6 +311,29 @@ | | | - os.tmpnam | | +------+---------------------+------------------------------------+-----------+ +B326: ast_overflow +------------------- + +It is possible to crash the Python interpreter by passing sufficiently +large/complex strings to ast.literal_eval(), ast.parse(), compile(), +dbm.dumb.open(), eval() or exec() due to stack depth limitations in Python’s +AST compiler. Ensure these functions are not used on untrusted data. + +For further information: + https://docs.python.org/3.7/library/ast.html#ast.parse + https://bugs.python.org/issue32758 + ++------+---------------------+------------------------------------+-----------+ +| ID | Name | Calls | Severity | ++======+=====================+====================================+===========+ +| B326 | ast_overflow | - ast.literal_eval | Low | +| | | - ast.parse | | +| | | - compile | | +| | | - dbm.dumb.open | | +| | | - eval | | +| | | - exec | | ++------+---------------------+------------------------------------+-----------+ + """ from bandit.blacklists import utils @@ -569,4 +592,19 @@ def gen_blacklist(): 'attacks. Consider using tmpfile() instead.' )) + sets.append(utils.build_conf_dict( + 'ast_overflow', 'B326', + ['ast.literal_eval', + 'ast.parse', + 'compile', + 'dbm.dumb.open', + 'eval'], + 'It is possible to crash the Python interpreter by passing ' + 'sufficiently large/complex strings to ast.literal_eval(), ' + 'ast.parse(), compile(), dbm.dumb.open(), eval() or exec() due to ' + 'stack depth limitations in Python’s AST compiler. Ensure these ' + 'functions are not used on untrusted data.', + 'LOW' + )) + return {'Call': sets} diff --git a/bandit/plugins/ast_overflow_exec.py b/bandit/plugins/ast_overflow_exec.py new file mode 100644 index 000000000..b534bbe9f --- /dev/null +++ b/bandit/plugins/ast_overflow_exec.py @@ -0,0 +1,43 @@ +# -*- coding:utf-8 -*- +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import six + +import bandit +from bandit.core import test_properties as test + + +def ast_overflow_exec_issue(): + return bandit.Issue( + severity=bandit.LOW, + confidence=bandit.HIGH, + text=('It is possible to crash the Python interpreter by passing ' + 'sufficiently large/complex strings to ast.literal_eval(), ' + 'ast.parse(), compile(), dbm.dumb.open(), eval() or exec() due ' + 'to stack depth limitations in Python’s AST compiler. Ensure ' + 'these functions are not used on untrusted data.') + ) + + +if six.PY2: + @test.checks('Exec') + @test.test_id('B326') + def ast_overflow(context): + return ast_overflow_exec_issue() +else: + @test.checks('Call') + @test.test_id('B326') + def ast_overflow(context): + if context.call_function_name_qual == 'exec': + return ast_overflow_exec_issue() diff --git a/examples/ast_overflow-py2.py b/examples/ast_overflow-py2.py new file mode 100644 index 000000000..e67821204 --- /dev/null +++ b/examples/ast_overflow-py2.py @@ -0,0 +1,16 @@ +import ast +import dbm.dumb + + +ast.literal_eval('x = 2 + 2') + +ast.parse('x = 2 + 2') + +compile('2 + 2', '?', 'eval') + +dbm.dumb.open('test.db') + +eval('2 + 2') + +exec('2 + 2') +exec '2 + 2' diff --git a/examples/ast_overflow-py3.py b/examples/ast_overflow-py3.py new file mode 100644 index 000000000..feef65ea6 --- /dev/null +++ b/examples/ast_overflow-py3.py @@ -0,0 +1,15 @@ +import ast +import dbm.dumb + + +ast.literal_eval('x = 2 + 2') + +ast.parse('x = 2 + 2') + +compile('2 + 2', '?', 'eval') + +dbm.dumb.open('test.db') + +eval('2 + 2') + +exec('2 + 2') diff --git a/setup.cfg b/setup.cfg index f0ec29c01..d9ff1c6a2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -122,6 +122,9 @@ bandit.plugins = # bandit/plugins/ssh_no_host_key_verification.py ssh_no_host_key_verification = bandit.plugins.ssh_no_host_key_verification:ssh_no_host_key_verification + # bandit/plugins/ast_overflow_exec.py + ast_overflow = bandit.plugins.ast_overflow_exec:ast_overflow + [build_sphinx] all_files = 1 build-dir = doc/build diff --git a/tests/functional/test_functional.py b/tests/functional/test_functional.py index e3b73702d..def6f9e35 100644 --- a/tests/functional/test_functional.py +++ b/tests/functional/test_functional.py @@ -165,6 +165,10 @@ def test_exec(self): 'CONFIDENCE': {'UNDEFINED': 0, 'LOW': 0, 'MEDIUM': 0, 'HIGH': 1} } + self.b_mgr.b_ts = b_test_set.BanditTestSet( + config=self.b_mgr.b_conf, + profile={'exclude': ['B326']} + ) self.check_example(filename, expect) def test_hardcoded_passwords(self): @@ -263,6 +267,29 @@ def test_tempnam(self): } self.check_example('tempnam.py', expect) + def test_ast_overflow(self): + '''Test for functions susceptible to AST stack overflow.''' + filename = 'ast_overflow-{}.py' + if six.PY2: + filename = filename.format('py2') + expect = { + 'SEVERITY': {'UNDEFINED': 0, 'LOW': 7, 'MEDIUM': 0, 'HIGH': 0}, + 'CONFIDENCE': { + 'UNDEFINED': 0, 'LOW': 0, 'MEDIUM': 0, 'HIGH': 7} + } + else: + filename = filename.format('py3') + expect = { + 'SEVERITY': {'UNDEFINED': 0, 'LOW': 6, 'MEDIUM': 0, 'HIGH': 0}, + 'CONFIDENCE': { + 'UNDEFINED': 0, 'LOW': 0, 'MEDIUM': 0, 'HIGH': 6} + } + self.b_mgr.b_ts = b_test_set.BanditTestSet( + config=self.b_mgr.b_conf, + profile={'exclude': ['B102', 'B307']} + ) + self.check_example(filename, expect) + def test_nonsense(self): '''Test that a syntactically invalid module is skipped.''' self.run_example('nonsense.py')