Breaking out blacklists

This change removes the old blacklist plugins and replaces them
with new built in functionality that loads blacklist item data
from a new plugin entry point. The new test also improve on the
old functionality that was broken in the following way:

import xml.sax # issue found OK
from xml import sax # no issue found, wrong

Finally, this patch removes the use of filename style wild cards
such as * from the import blacklist matching, as this was not being
used. Both this test and the old ones will alert on any import from
within the blacklisted namespace.

Change-Id: I98af6daf3c54561c0e4b399605ea615b42b7b283
This commit is contained in:
Tim Kelsey 2016-01-16 00:25:15 +00:00
parent 541dc8928f
commit a9839d4266
15 changed files with 428 additions and 694 deletions

View File

225
bandit/blacklists/calls.py Normal file
View File

@ -0,0 +1,225 @@
# -*- coding:utf-8 -*-
#
# Copyright 2016 Hewlett-Packard Development Company, L.P.
#
# 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.
from bandit.blacklists import utils
def gen_blacklist():
"""Generate a list of items to blacklist.
Methods of this type, "bandit.blacklist" plugins, are used to build a list
of items that bandit's built in blacklisting tests will use to trigger
issues. They replace the older blacklist* test plugins and allow
blacklisted items to have a unique bandit ID for filtering and profile
usage.
:return: a dictionary mapping node types to a list of blacklist data
"""
sets = []
sets.append(utils.build_conf_dict(
'pickle', 'B301',
['pickle.loads',
'pickle.load',
'pickle.Unpickler',
'cPickle.loads',
'cPickle.load',
'cPickle.Unpickler'],
'Pickle library appears to be in use, possible security issue.'
))
sets.append(utils.build_conf_dict(
'marshal', 'B302', ['marshal.load', 'marshal.loads'],
'Deserialization with the marshal module is possibly dangerous.'
))
sets.append(utils.build_conf_dict(
'md5', 'B303',
['hashlib.md5',
'Crypto.Hash.MD2.new',
'Crypto.Hash.MD4.new',
'Crypto.Hash.MD5.new',
'cryptography.hazmat.primitives.hashes.MD5'],
'Use of insecure MD2, MD4, or MD5 hash function.'
))
sets.append(utils.build_conf_dict(
'ciphers', 'B304',
['Crypto.Cipher.ARC2.new',
'Crypto.Cipher.ARC4.new',
'Crypto.Cipher.Blowfish.new',
'Crypto.Cipher.DES.new',
'Crypto.Cipher.XOR.new',
'cryptography.hazmat.primitives.ciphers.algorithms.ARC4',
'cryptography.hazmat.primitives.ciphers.algorithms.Blowfish',
'cryptography.hazmat.primitives.ciphers.algorithms.IDEA'],
'Use of insecure cipher {name}. Replace with a known secure'
' cipher such as AES.',
'HIGH'
))
sets.append(utils.build_conf_dict(
'cipher_modes', 'B305',
['cryptography.hazmat.primitives.ciphers.modes.ECB'],
'Use of insecure cipher mode {name}.'
))
sets.append(utils.build_conf_dict(
'mktemp_q', 'B306', ['tempfile.mktemp'],
'Use of insecure and deprecated function (mktemp).'
))
sets.append(utils.build_conf_dict(
'eval', 'B307', ['eval'],
'Use of possibly insecure function - consider using safer '
'ast.literal_eval.'
))
sets.append(utils.build_conf_dict(
'mark_safe', 'B308', ['mark_safe'],
'Use of mark_safe() may expose cross-site scripting '
'vulnerabilities and should be reviewed.'
))
sets.append(utils.build_conf_dict(
'httpsconnection', 'B309',
['httplib.HTTPSConnection',
'http.client.HTTPSConnection',
'six.moves.http_client.HTTPSConnection'],
'Use of HTTPSConnection does not provide security, see '
'https://wiki.openstack.org/wiki/OSSN/OSSN-0033'
))
sets.append(utils.build_conf_dict(
'urllib_urlopen', 'B310',
['urllib.urlopen',
'urllib.request.urlopen',
'urllib.urlretrieve',
'urllib.request.urlretrieve',
'urllib.URLopener',
'urllib.request.URLopener',
'urllib.FancyURLopener',
'urllib.request.FancyURLopener',
'urllib2.urlopen',
'urllib2.Request',
'six.moves.urllib.request.urlopen',
'six.moves.urllib.request.urlretrieve',
'six.moves.urllib.request.URLopener',
'six.moves.urllib.request.FancyURLopener'],
'Audit url open for permitted schemes. Allowing use of file:/ or '
'custom schemes is often unexpected.'
))
sets.append(utils.build_conf_dict(
'random', 'B311',
['random.random',
'random.randrange',
'random.randint',
'random.choice',
'random.uniform',
'random.triangular'],
'Standard pseudo-random generators are not suitable for '
'security/cryptographic purposes.',
'LOW'
))
sets.append(utils.build_conf_dict(
'telnetlib', 'B312', ['telnetlib.*'],
'Telnet-related funtions are being called. Telnet is considered '
'insecure. Use SSH or some other encrypted protocol.',
'HIGH'
))
# Most of this is based off of Christian Heimes' work on defusedxml:
# https://pypi.python.org/pypi/defusedxml/#defusedxml-sax
xml_msg = ('Using {name} to parse untrusted XML data is known to be '
'vulnerable to XML attacks. Replace {name} with its '
'defusedxml equivalent function.')
sets.append(utils.build_conf_dict(
'xml_bad_cElementTree', 'B313',
['xml.etree.cElementTree.parse',
'xml.etree.cElementTree.iterparse',
'xml.etree.cElementTree.fromstring',
'xml.etree.cElementTree.XMLParser'],
xml_msg
))
sets.append(utils.build_conf_dict(
'xml_bad_ElementTree', 'B314',
['xml.etree.ElementTree.parse',
'xml.etree.ElementTree.iterparse',
'xml.etree.ElementTree.fromstring',
'xml.etree.ElementTree.XMLParser'],
xml_msg
))
sets.append(utils.build_conf_dict(
'xml_bad_expatreader', 'B315', ['xml.sax.expatreader.create_parser'],
xml_msg
))
sets.append(utils.build_conf_dict(
'xml_bad_expatbuilder', 'B316',
['xml.dom.expatbuilder.parse',
'xml.dom.expatbuilder.parseString'],
xml_msg
))
sets.append(utils.build_conf_dict(
'xml_bad_sax', 'B317',
['xml.sax.parse',
'xml.sax.parseString',
'xml.sax.make_parser'],
xml_msg
))
sets.append(utils.build_conf_dict(
'xml_bad_minidom', 'B318',
['xml.dom.minidom.parse',
'xml.dom.minidom.parseString'],
xml_msg
))
sets.append(utils.build_conf_dict(
'xml_bad_pulldom', 'B319',
['xml.dom.pulldom.parse',
'xml.dom.pulldom.parseString'],
xml_msg
))
sets.append(utils.build_conf_dict(
'xml_bad_etree', 'B320',
['lxml.etree.parse',
'lxml.etree.fromstring',
'lxml.etree.RestrictedElement',
'lxml.etree.GlobalParserTLS',
'lxml.etree.getDefaultParser',
'lxml.etree.check_docinfo'],
xml_msg
))
# end of XML tests
sets.append(utils.build_conf_dict(
'ftplib', 'B321', ['ftplib.*'],
'FTP-related funtions are being called. FTP is considered '
'insecure. Use SSH/SFTP/SCP or some other encrypted protocol.',
'HIGH'
))
return {'Call': sets}

View File

@ -0,0 +1,81 @@
# -*- coding:utf-8 -*-
#
# Copyright 2016 Hewlett-Packard Development Company, L.P.
#
# 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.
from bandit.blacklists import utils
def gen_blacklist():
"""Generate a list of items to blacklist.
Methods of this type, "bandit.blacklist" plugins, are used to build a list
of items that bandit's built in blacklisting tests will use to trigger
issues. They replace the older blacklist* test plugins and allow
blacklisted items to have a unique bandit ID for filtering and profile
usage.
:return: a dictionary mapping node types to a list of blacklist data
"""
sets = []
sets.append(utils.build_conf_dict(
'telnet', 'B401', ['telnetlib'],
'A telnet-related module is being imported. Telnet is '
'considered insecure. Use SSH or some other encrypted protocol.',
'HIGH'
))
sets.append(utils.build_conf_dict(
'ftp', 'B402', ['ftplib'],
'A FTP-related module is being imported. FTP is considered '
'insecure. Use SSH/SFTP/SCP or some other encrypted protocol.',
'HIGH'
))
sets.append(utils.build_conf_dict(
'info_libs', 'B403', ['pickle', 'cPickle', 'subprocess', 'Crypto'],
'Consider possible security implications associated with '
'{name} module.', 'LOW'
))
# Most of this is based off of Christian Heimes' work on defusedxml:
# https://pypi.python.org/pypi/defusedxml/#defusedxml-sax
sets.append(utils.build_conf_dict(
'xml_libs', 'B404',
['xml.etree.cElementTree',
'xml.etree.ElementTree',
'xml.sax.expatreader',
'xml.sax',
'xml.dom.expatbuilder',
'xml.dom.minidom',
'xml.dom.pulldom',
'lxml.etree',
'lxml'],
'Using {name} to parse untrusted XML data is known to be '
'vulnerable to XML attacks. Replace {name} with the equivalent '
'defusedxml package.', 'LOW'
))
sets.append(utils.build_conf_dict(
'xml_libs_high', 'B405', ['xmlrpclib'],
'Using {name} to parse untrusted XML data is known to be '
'vulnerable to XML attacks. Use defused.xmlrpc.monkey_patch() '
'function to monkey-patch xmlrpclib and mitigate XML '
'vulnerabilities.', 'HIGH'
))
return {'Import': sets, 'ImportFrom': sets, 'Call': sets}

View File

@ -0,0 +1,22 @@
# -*- coding:utf-8 -*-
#
# Copyright 2016 Hewlett-Packard Development Company, L.P.
#
# 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.
def build_conf_dict(name, bid, qualnames, message, level='MEDIUM'):
"""Build and return a blacklist configuration dict."""
return {'name': name, 'id': bid, 'message': message,
'qualnames': qualnames, 'level': level}

View File

@ -0,0 +1,61 @@
# -*- coding:utf-8 -*-
#
# Copyright 2016 Hewlett-Packard Development Company, L.P.
#
# 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 ast
import fnmatch
from bandit.core import extension_loader
from bandit.core import issue
def report_issue(check, name):
return issue.Issue(
severity=check['level'], confidence='HIGH',
text=check['message'].replace('{name}', name),
ident=name, test_id=check["id"])
def blacklist(context):
blacklists = extension_loader.MANAGER.blacklist
node_type = context.node.__class__.__name__
if node_type not in blacklists:
return
if node_type == 'Call':
func = context.node.func
if isinstance(func, ast.Name) and func.id == '__import__':
if len(context.node.args):
name = context.node.args[0].s
else:
name = "" # handle '__import__()'
else:
name = context.call_function_name_qual
for check in blacklists[node_type]:
for qn in check['qualnames']:
if fnmatch.fnmatch(name, qn):
return report_issue(check, name)
if node_type.startswith('Import'):
prefix = ""
if node_type == "ImportFrom":
if context.node.module is not None:
prefix = context.node.module + "."
for check in blacklists[node_type]:
for name in context.node.names:
for qn in check['qualnames']:
if (prefix + name.name).startswith(qn):
return report_issue(check, name.name)

View File

@ -16,15 +16,18 @@ from __future__ import print_function
import sys
import six
from stevedore import extension
class Manager(object):
def __init__(self, formatters_namespace='bandit.formatters',
plugins_namespace='bandit.plugins'):
plugins_namespace='bandit.plugins',
blacklists_namespace='bandit.blacklists'):
# Cache the extension managers, loaded extensions, and extension names
self.load_formatters(formatters_namespace)
self.load_plugins(plugins_namespace)
self.load_blacklists(blacklists_namespace)
def load_formatters(self, formatters_namespace):
self.formatters_mgr = extension.ExtensionManager(
@ -63,6 +66,18 @@ class Manager(object):
def get_plugin_id(self, plugin_name):
return self.plugin_name_to_id.get(plugin_name)
def load_blacklists(self, blacklist_namespace):
self.blacklists_mgr = extension.ExtensionManager(
namespace=blacklist_namespace,
invoke_on_load=False,
verify_requirements=False,
)
self.blacklist = {}
blacklist = list(self.blacklists_mgr)
for item in blacklist:
for key, val in six.iteritems(item.plugin()):
self.blacklist.setdefault(key, []).extend(val)
# Using entry-points and pkg_resources *can* be expensive. So let's load these
# once, store them on the object, and have a module global object for
# accessing them. After the first time this module is imported, it should save

View File

@ -25,14 +25,14 @@ import linecache
class Issue(object):
def __init__(self, severity, confidence=constants.CONFIDENCE_DEFAULT,
text="", ident=None, lineno=None):
text="", ident=None, lineno=None, test_id=""):
self.severity = severity
self.confidence = confidence
self.text = text
self.ident = ident
self.fname = ""
self.test = ""
self.test_id = ""
self.test_id = test_id
self.lineno = lineno
self.linerange = []

View File

@ -22,6 +22,8 @@ import logging
import sys
import warnings
from bandit.core import blacklisting
from bandit.core import extension_loader
from bandit.core import utils
@ -37,6 +39,11 @@ class BanditTestSet():
filter_list = self._filter_list_from_config(profile=profile)
self.load_tests(filter=filter_list)
# load blacklists
for key in extension_loader.MANAGER.blacklist.keys():
value = self.tests.setdefault(key, {})
value["blacklist"] = blacklisting.blacklist
def _filter_list_from_config(self, profile=None):
# will create an (include,exclude) list tuple from a specified name
# config section

View File

@ -74,7 +74,8 @@ class BanditTester():
result.lineno = temp_context['lineno']
result.linerange = temp_context['linerange']
result.test = test.__name__
result.test_id = test._test_id
if result.test_id == "":
result.test_id = test._test_id
self.results.append(result)

View File

@ -1,379 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# 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.
r"""
=================================
B301: Test for black listed calls
=================================
A number of Python methods and functions are known to have potential security
implications. The blacklist calls plugin test is designed to detect the use of
these methods by scanning code for method calls and checking for their presence
in a configurable blacklist. The scanned calls are fully qualified and
de-aliased prior to checking. To illustrate this, imagine a check for
"evil.thing()" running on the following example code:
.. code-block:: python
import evil as good
good.thing()
thing()
This would generate a warning about calling `evil.thing()` despite the module
being aliased as `good`. It would also not generate a warning on the call to
`thing()` in the local module, as it's fully qualified name will not match.
Each of the provided blacklisted calls can be grouped such that they generate
appropriate warnings (message, severity) and a token `{func}` may be used
in the provided output message, to be replaced with the actual method name.
Due to the nature of the test, confidence is always reported as HIGH
**Config Options:**
.. code-block:: yaml
blacklist_calls:
bad_name_sets:
- pickle:
qualnames:
- pickle.loads
- pickle.load
- pickle.Unpickler
- cPickle.loads
- cPickle.load
- cPickle.Unpickler
message: >
Pickle library appears to be in use, possible security
issue.
- marshal:
qualnames: [marshal.load, marshal.loads]
message: >
Deserialization with the {func} is possibly dangerous.
level: LOW
:Example:
.. code-block:: none
>> Issue: Pickle library appears to be in use, possible security issue.
Severity: Medium Confidence: High
Location: ./examples/pickle_deserialize.py:20
19 serialized = cPickle.dumps({(): []})
20 print(cPickle.loads(serialized))
21
.. seealso::
- https://security.openstack.org
.. versionadded:: 0.9.0
"""
import fnmatch
import bandit
from bandit.core import test_properties as test
_cached_blacklist_checks = []
_cached_blacklist_config = None # FIXME(tkelsey): there is no point in this ..
def _build_conf_dict(name, qualnames, message, level='MEDIUM'):
return {name: {'message': message, 'qualnames': qualnames, 'level': level}}
def gen_config(name):
if 'blacklist_calls' == name:
sets = []
sets.append(_build_conf_dict(
'pickle',
['pickle.loads',
'pickle.load',
'pickle.Unpickler',
'cPickle.loads',
'cPickle.load',
'cPickle.Unpickler'],
'Pickle library appears to be in use, possible security issue.'
))
sets.append(_build_conf_dict(
'marshal', ['marshal.load', 'marshal.loads'],
'Deserialization with the marshal module is possibly dangerous.'
))
sets.append(_build_conf_dict(
'md5',
['hashlib.md5',
'Crypto.Hash.MD2.new',
'Crypto.Hash.MD4.new',
'Crypto.Hash.MD5.new',
'cryptography.hazmat.primitives.hashes.MD5'],
'Use of insecure MD2, MD4, or MD5 hash function.'
))
sets.append(_build_conf_dict(
'ciphers',
['Crypto.Cipher.ARC2.new',
'Crypto.Cipher.ARC4.new',
'Crypto.Cipher.Blowfish.new',
'Crypto.Cipher.DES.new',
'Crypto.Cipher.XOR.new',
'cryptography.hazmat.primitives.ciphers.algorithms.ARC4',
'cryptography.hazmat.primitives.ciphers.algorithms.Blowfish',
'cryptography.hazmat.primitives.ciphers.algorithms.IDEA'],
'Use of insecure cipher {func}. Replace with a known secure'
' cipher such as AES.',
'HIGH'
))
sets.append(_build_conf_dict(
'cipher_modes',
['cryptography.hazmat.primitives.ciphers.modes.ECB'],
'Use of insecure cipher mode {func}.'
))
sets.append(_build_conf_dict(
'mktemp_q', ['tempfile.mktemp'],
'Use of insecure and deprecated function (mktemp).'
))
sets.append(_build_conf_dict(
'eval', ['eval'],
'Use of possibly insecure function - consider using safer '
'ast.literal_eval.'
))
sets.append(_build_conf_dict(
'mark_safe', ['mark_safe'],
'Use of mark_safe() may expose cross-site scripting '
'vulnerabilities and should be reviewed.'
))
sets.append(_build_conf_dict(
'httpsconnection',
['httplib.HTTPSConnection',
'http.client.HTTPSConnection',
'six.moves.http_client.HTTPSConnection'],
'Use of HTTPSConnection does not provide security, see '
'https://wiki.openstack.org/wiki/OSSN/OSSN-0033'
))
sets.append(_build_conf_dict(
'urllib_urlopen',
['urllib.urlopen',
'urllib.request.urlopen',
'urllib.urlretrieve',
'urllib.request.urlretrieve',
'urllib.URLopener',
'urllib.request.URLopener',
'urllib.FancyURLopener',
'urllib.request.FancyURLopener',
'urllib2.urlopen',
'urllib2.Request',
'six.moves.urllib.request.urlopen',
'six.moves.urllib.request.urlretrieve',
'six.moves.urllib.request.URLopener',
'six.moves.urllib.request.FancyURLopener'],
'Audit url open for permitted schemes. Allowing use of file:/ or '
'custom schemes is often unexpected.'
))
sets.append(_build_conf_dict(
'random',
['random.random',
'random.randrange',
'random.randint',
'random.choice',
'random.uniform',
'random.triangular'],
'Standard pseudo-random generators are not suitable for '
'security/cryptographic purposes.',
'LOW'
))
sets.append(_build_conf_dict(
'telnetlib', ['telnetlib.*'],
'Telnet-related funtions are being called. Telnet is considered '
'insecure. Use SSH or some other encrypted protocol.',
'HIGH'
))
sets.append(_build_conf_dict(
'ftplib', ['ftplib.*'],
'FTP-related funtions are being called. FTP is considered '
'insecure. Use SSH/SFTP/SCP or some other encrypted protocol.',
'HIGH'
))
# Most of this is based off of Christian Heimes' work on defusedxml:
# https://pypi.python.org/pypi/defusedxml/#defusedxml-sax
xml_msg = ('Using {func} to parse untrusted XML data is known to be '
'vulnerable to XML attacks. Replace {func} with its '
'defusedxml equivalent function.')
sets.append(_build_conf_dict(
'xml_bad_cElementTree',
['xml.etree.cElementTree.parse',
'xml.etree.cElementTree.iterparse',
'xml.etree.cElementTree.fromstring',
'xml.etree.cElementTree.XMLParser'],
xml_msg
))
sets.append(_build_conf_dict(
'xml_bad_ElementTree',
['xml.etree.ElementTree.parse',
'xml.etree.ElementTree.iterparse',
'xml.etree.ElementTree.fromstring',
'xml.etree.ElementTree.XMLParser'],
xml_msg
))
sets.append(_build_conf_dict(
'xml_bad_expatreader', ['xml.sax.expatreader.create_parser'],
xml_msg
))
sets.append(_build_conf_dict(
'xml_bad_expatbuilder',
['xml.dom.expatbuilder.parse',
'xml.dom.expatbuilder.parseString'],
xml_msg
))
sets.append(_build_conf_dict(
'xml_bad_sax',
['xml.sax.parse',
'xml.sax.parseString',
'xml.sax.make_parser'],
xml_msg
))
sets.append(_build_conf_dict(
'xml_bad_minidom',
['xml.dom.minidom.parse',
'xml.dom.minidom.parseString'],
xml_msg
))
sets.append(_build_conf_dict(
'xml_bad_pulldom',
['xml.dom.pulldom.parse',
'xml.dom.pulldom.parseString'],
xml_msg
))
sets.append(_build_conf_dict(
'xml_bad_etree',
['lxml.etree.parse',
'lxml.etree.fromstring',
'lxml.etree.RestrictedElement',
'lxml.etree.GlobalParserTLS',
'lxml.etree.getDefaultParser',
'lxml.etree.check_docinfo'],
xml_msg
))
return {'bad_name_sets': sets}
@test.takes_config
@test.checks('Call')
@test.test_id('B301')
def blacklist_calls(context, config):
_ensure_cache(config)
checks = _cached_blacklist_checks
# for each check, go through and see if it matches all qualifications
for qualnames, names, message_tpl, level, params in checks:
confidence = 'HIGH'
does_match = True
# item 0=qualnames, 1=names, 2=message, 3=level, 4=params
if does_match and qualnames:
# match the qualname - respect wildcards if present
does_match = any(
fnmatch.fnmatch(context.call_function_name_qual, qn)
for qn in qualnames)
if does_match and names:
does_match = any(context.call_function_name == n for n in names)
if does_match and params:
matched_p = False
for p in params:
for arg_num in range(0, context.call_args_count - 1):
if p == context.get_call_arg_at_position(arg_num):
matched_p = True
if not matched_p:
does_match = False
if does_match:
message = message_tpl.replace("{func}",
context.call_function_name_qual)
return bandit.Issue(
severity=level, confidence=confidence,
text=message,
ident=context.call_function_name_qual
)
def _ensure_cache(config):
global _cached_blacklist_config
if _cached_blacklist_checks and config is _cached_blacklist_config:
return
_cached_blacklist_config = config
if config is not None and 'bad_name_sets' in config:
sets = config['bad_name_sets']
else:
sets = []
# load all the checks from the config file
for cur_item in sets:
for blacklist_item in cur_item:
blacklist_object = cur_item[blacklist_item]
cur_check = _get_tuple_for_item(blacklist_object)
# skip bogus checks
if cur_check:
_cached_blacklist_checks.append(cur_check)
def _get_tuple_for_item(blacklist_object):
level_map = {'LOW': bandit.LOW, 'MEDIUM': bandit.MEDIUM,
'HIGH': bandit.HIGH}
# if the item we got passed isn't a dictionary, do nothing with this object
if not isinstance(blacklist_object, dict):
return None
# not all of the fields will be set, so all have default fallbacks
qualnames = blacklist_object.get('qualnames')
names = blacklist_object.get('names')
message = blacklist_object.get('message', '')
params = blacklist_object.get('params')
level_name = blacklist_object.get('level', 'MEDIUM').upper()
level = level_map.get(level_name, 'MEDIUM')
return (qualnames, names, message, level, params)

View File

@ -1,296 +0,0 @@
# -*- coding:utf-8 -*-
#
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# 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 bandit
from bandit.core import test_properties as test
def _build_conf_dict(name, imports, message, level='MEDIUM'):
return {name: {'message': message, 'imports': imports, 'level': level}}
def gen_config(name):
if 'blacklist_imports' == name:
sets = []
sets.append(_build_conf_dict(
'telnet', ['telnetlib'],
'A telnet-related module is being imported. Telnet is '
'considered insecure. Use SSH or some other encrypted protocol.',
'HIGH'
))
sets.append(_build_conf_dict(
'ftp', ['ftplib'],
'A FTP-related module is being imported. FTP is considered '
'insecure. Use SSH/SFTP/SCP or some other encrypted protocol.',
'HIGH'
))
sets.append(_build_conf_dict(
'info_libs', ['pickle', 'cPickle', 'subprocess', 'Crypto'],
'Consider possible security implications associated with '
'{module} module.', 'LOW'
))
# Most of this is based off of Christian Heimes' work on defusedxml:
# https://pypi.python.org/pypi/defusedxml/#defusedxml-sax
sets.append(_build_conf_dict(
'xml_libs',
['xml.etree.cElementTree',
'xml.etree.ElementTree',
'xml.sax.expatreader',
'xml.sax',
'xml.dom.expatbuilder',
'xml.dom.minidom',
'xml.dom.pulldom',
'lxml.etree',
'lxml'],
'Using {module} to parse untrusted XML data is known to be '
'vulnerable to XML attacks. Replace {module} with the equivalent '
'defusedxml package.', 'LOW'
))
sets.append(_build_conf_dict(
'xml_libs_high', ['xmlrpclib'],
'Using {module} to parse untrusted XML data is known to be '
'vulnerable to XML attacks. Use defused.xmlrpc.monkey_patch() '
'function to monkey-patch xmlrpclib and mitigate XML '
'vulnerabilities.', 'HIGH'
))
return {'bad_import_sets': sets}
@test.takes_config
@test.checks('Import', 'ImportFrom')
@test.test_id('B401')
def blacklist_imports(context, config):
"""**B401: Test for blacklisted imports**
A number of Python modules are known to provide collections of
functionality with potential security implications. The blacklist imports
plugin test is designed to detect the use of these modules by scanning code
for `import` statements and checking for the imported modules presence in a
configurable blacklist. The imported modules are fully qualified and
de-aliased prior to checking. To illustrate this, imagine a check for
"module.evil" running on the following example code:
.. code-block:: python
import module # no warning
import module.evil # warning
from module import evil # warning
from module import evil as good # warning
This would generate a warning about importing `module.evil` in each of the
last three cases, despite the module being aliased as `good` in one of
them. It would also not generate a warning on the first import
(of `module`) as it's fully qualified name will not match.
Each of the provided blacklisted modules can be grouped such that they
generate appropriate warnings (message, severity) and a token `{module}`
may be used in the provided output message, to be replaced with the actual
module name.
Due to the nature of the test, confidence is always reported as HIGH
**Config Options:**
.. code-block:: yaml
blacklist_imports:
bad_import_sets:
- xml_libs:
imports:
- xml.etree.cElementTree
- xml.etree.ElementTree
- xml.sax.expatreader
- xml.sax
- xml.dom.expatbuilder
- xml.dom.minidom
- xml.dom.pulldom
- lxml.etree
- lxml
message: >
Using {module} to parse untrusted XML data is known to
be vulnerable to XML attacks. Replace {module} with the
equivalent defusedxml package.
level: LOW
:Example:
.. code-block:: none
>> Issue: Using xml.sax to parse untrusted XML data is known to be
vulnerable to XML attacks. Replace xml.sax with the equivalent
defusedxml package.
Severity: Low Confidence: High
Location: ./examples/xml_sax.py:1
1 import xml.sax
2 from xml import sax
>> Issue: Using xml.sax.parseString to parse untrusted XML data is
known to be vulnerable to XML attacks. Replace xml.sax.parseString with
its defusedxml equivalent function.
Severity: Medium Confidence: High
Location: ./examples/xml_sax.py:21
20 # bad
21 xml.sax.parseString(xmlString, ExampleContentHandler())
22 xml.sax.parse('notaxmlfilethatexists.xml', ExampleContentHandler())
.. seealso::
- https://security.openstack.org
.. versionadded:: 0.9.0
"""
checks = _load_checks(config)
# for each check, go through and see if it matches all qualifications
for check in checks:
# item 0=import, 1=message, 2=level
if check[0]:
for im in check[0]:
if context.is_module_being_imported(im):
return _get_result(check, im)
@test.takes_config('blacklist_imports')
@test.checks('Call')
@test.test_id('B402')
def blacklist_import_func(context, config):
"""**B402: Test for blacklisted import functions**
This test is in all ways identical blacklist_imports. However, it
is designed to catch modules that have been imported using Python's special
builtin import function, `__import__()`. For example, running a test on the
following code for `module.evil` would warn as shown:
.. code-block:: python
__import__('module') # no warning
__import__('module.evil') # warning
This test shares the configuration provided for the standard
blacklist_imports test.
:Example:
.. code-block:: none
>> Issue: Using xml.sax to parse untrusted XML data is known to be
vulnerable to XML attacks. Replace xml.sax with the equivalent
defusedxml package.
Severity: Low Confidence: High
Location: ./examples/xml_sax.py:1
1 import xml.sax
2 from xml import sax
>> Issue: Using xml.sax.parseString to parse untrusted XML data is
known to be vulnerable to XML attacks. Replace xml.sax.parseString with
its defusedxml equivalent function.
Severity: Medium Confidence: High
Location: ./examples/xml_sax.py:21
20 # bad
21 xml.sax.parseString(xmlString, ExampleContentHandler())
22 xml.sax.parse('notaxmlfilethatexists.xml', ExampleContentHandler())
.. seealso::
- https://security.openstack.org
.. versionadded:: 0.9.0
"""
checks = _load_checks(config)
if context.call_function_name_qual == '__import__':
for check in checks:
# item 0=import, 1=message, 2=level
if check[0]:
for im in check[0]:
if len(context.call_args) and im == context.call_args[0]:
return _get_result(check, im)
def _load_checks(config):
# load all the checks from the config file
if config is not None and 'bad_import_sets' in config:
sets = config['bad_import_sets']
else:
sets = []
checks = []
for cur_item in sets:
for blacklist_item in cur_item:
blacklist_object = cur_item[blacklist_item]
cur_check = _get_tuple_for_item(blacklist_object)
# skip bogus checks
if cur_check:
checks.append(cur_check)
return checks
def _get_tuple_for_item(blacklist_object):
# default values
imports = None
message = ""
level = 'MEDIUM'
# if the item we got passed isn't a dictionary, do nothing with the object;
# if the item we got passed doesn't have an imports field, we can't do
# anything with this. Return None
if (not isinstance(blacklist_object, dict) or
'imports' not in blacklist_object):
return None
imports = blacklist_object['imports']
if 'message' in blacklist_object:
message = blacklist_object['message']
if 'level' in blacklist_object:
if blacklist_object['level'] == 'HIGH':
level = 'HIGH'
elif blacklist_object['level'] == 'MEDIUM':
level = 'MEDIUM'
elif blacklist_object['level'] == 'LOW':
level = 'LOW'
return_tuple = (imports, message, level)
return return_tuple
def _get_result(check, im):
# substitute '{module}' for the imported module name
message = check[1].replace('{module}', im)
level = None
if check[2] == 'HIGH':
level = bandit.HIGH
elif check[2] == 'MEDIUM':
level = bandit.MEDIUM
elif check[2] == 'LOW':
level = bandit.LOW
return bandit.Issue(severity=level, confidence=bandit.HIGH, text=message)

View File

@ -26,6 +26,9 @@ console_scripts =
bandit = bandit.cli.main:main
bandit-config-generator = bandit.cli.config_generator:main
bandit-baseline = bandit.cli.baseline:main
bandit.blacklists =
calls = bandit.blacklists.calls:gen_blacklist
imports = bandit.blacklists.imports:gen_blacklist
bandit.formatters =
csv = bandit.formatters.csv:report
json = bandit.formatters.json:report
@ -40,13 +43,6 @@ bandit.plugins =
# bandit/plugins/asserts.py
assert_used = bandit.plugins.asserts:assert_used
# bandit/plugins/blacklist_calls.py
blacklist_calls = bandit.plugins.blacklist_calls:blacklist_calls
# bandit/plugins/blacklist_imports.py
blacklist_imports = bandit.plugins.blacklist_imports:blacklist_imports
blacklist_import_func = bandit.plugins.blacklist_imports:blacklist_import_func
# bandit/plugins/crypto_request_no_cert_validation.py
request_with_no_cert_validation = bandit.plugins.crypto_request_no_cert_validation:request_with_no_cert_validation

View File

@ -116,12 +116,14 @@ class FunctionalTests(testtools.TestCase):
def test_crypto_md5(self):
'''Test the `hashlib.md5` example.'''
expect = {'SEVERITY': {'MEDIUM': 8}, 'CONFIDENCE': {'HIGH': 8}}
expect = {'SEVERITY': {'MEDIUM': 8, 'LOW': 3},
'CONFIDENCE': {'HIGH': 11}}
self.check_example('crypto-md5.py', expect)
def test_ciphers(self):
'''Test the `Crypto.Cipher` example.'''
expect = {'SEVERITY': {'LOW': 1, 'HIGH': 8}, 'CONFIDENCE': {'HIGH': 9}}
expect = {'SEVERITY': {'LOW': 8, 'HIGH': 8},
'CONFIDENCE': {'HIGH': 16}}
self.check_example('ciphers.py', expect)
def test_cipher_modes(self):
@ -384,8 +386,8 @@ class FunctionalTests(testtools.TestCase):
'CONFIDENCE': {'HIGH': 2, 'MEDIUM': 2}}
self.check_example('xml_minidom.py', expect)
expect = {'SEVERITY': {'LOW': 1, 'HIGH': 6},
'CONFIDENCE': {'HIGH': 1, 'MEDIUM': 6}}
expect = {'SEVERITY': {'LOW': 2, 'HIGH': 6},
'CONFIDENCE': {'HIGH': 2, 'MEDIUM': 6}}
self.check_example('xml_sax.py', expect)
def test_asserts(self):
@ -436,8 +438,8 @@ class FunctionalTests(testtools.TestCase):
def test_weak_cryptographic_key(self):
'''Test for weak key sizes.'''
expect = {
'SEVERITY': {'MEDIUM': 5, 'HIGH': 4},
'CONFIDENCE': {'HIGH': 9}
'SEVERITY': {'LOW': 2, 'MEDIUM': 5, 'HIGH': 4},
'CONFIDENCE': {'HIGH': 11}
}
self.check_example('weak_cryptographic_key_sizes.py', expect)

View File

@ -96,7 +96,7 @@ class RuntimeTests(testtools.TestCase):
self.assertIn("Low: 2", output)
self.assertIn("High: 2", output)
self.assertIn("Files skipped (0):", output)
self.assertIn("Issue: [B401:blacklist_imports] Consider possible",
self.assertIn("Issue: [B403:blacklist] Consider possible",
output)
self.assertIn("imports.py:2", output)
self.assertIn("imports.py:4", output)

View File

@ -131,7 +131,6 @@ class ManagerTests(testtools.TestCase):
self.assertEqual(m.debug, False)
self.assertEqual(m.verbose, False)
self.assertEqual(m.agg_type, 'file')
self.assertFalse(m.has_tests)
def test_matches_globlist(self):
self.assertTrue(manager._matches_glob_list('test', ['*tes*']))