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:
parent
541dc8928f
commit
a9839d4266
0
bandit/blacklists/__init__.py
Normal file
0
bandit/blacklists/__init__.py
Normal file
225
bandit/blacklists/calls.py
Normal file
225
bandit/blacklists/calls.py
Normal 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}
|
81
bandit/blacklists/imports.py
Normal file
81
bandit/blacklists/imports.py
Normal 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}
|
22
bandit/blacklists/utils.py
Normal file
22
bandit/blacklists/utils.py
Normal 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}
|
61
bandit/core/blacklisting.py
Normal file
61
bandit/core/blacklisting.py
Normal 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)
|
@ -16,15 +16,18 @@ from __future__ import print_function
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
import six
|
||||||
from stevedore import extension
|
from stevedore import extension
|
||||||
|
|
||||||
|
|
||||||
class Manager(object):
|
class Manager(object):
|
||||||
def __init__(self, formatters_namespace='bandit.formatters',
|
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
|
# Cache the extension managers, loaded extensions, and extension names
|
||||||
self.load_formatters(formatters_namespace)
|
self.load_formatters(formatters_namespace)
|
||||||
self.load_plugins(plugins_namespace)
|
self.load_plugins(plugins_namespace)
|
||||||
|
self.load_blacklists(blacklists_namespace)
|
||||||
|
|
||||||
def load_formatters(self, formatters_namespace):
|
def load_formatters(self, formatters_namespace):
|
||||||
self.formatters_mgr = extension.ExtensionManager(
|
self.formatters_mgr = extension.ExtensionManager(
|
||||||
@ -63,6 +66,18 @@ class Manager(object):
|
|||||||
def get_plugin_id(self, plugin_name):
|
def get_plugin_id(self, plugin_name):
|
||||||
return self.plugin_name_to_id.get(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
|
# 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
|
# 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
|
# accessing them. After the first time this module is imported, it should save
|
||||||
|
@ -25,14 +25,14 @@ import linecache
|
|||||||
|
|
||||||
class Issue(object):
|
class Issue(object):
|
||||||
def __init__(self, severity, confidence=constants.CONFIDENCE_DEFAULT,
|
def __init__(self, severity, confidence=constants.CONFIDENCE_DEFAULT,
|
||||||
text="", ident=None, lineno=None):
|
text="", ident=None, lineno=None, test_id=""):
|
||||||
self.severity = severity
|
self.severity = severity
|
||||||
self.confidence = confidence
|
self.confidence = confidence
|
||||||
self.text = text
|
self.text = text
|
||||||
self.ident = ident
|
self.ident = ident
|
||||||
self.fname = ""
|
self.fname = ""
|
||||||
self.test = ""
|
self.test = ""
|
||||||
self.test_id = ""
|
self.test_id = test_id
|
||||||
self.lineno = lineno
|
self.lineno = lineno
|
||||||
self.linerange = []
|
self.linerange = []
|
||||||
|
|
||||||
|
@ -22,6 +22,8 @@ import logging
|
|||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
from bandit.core import blacklisting
|
||||||
|
from bandit.core import extension_loader
|
||||||
from bandit.core import utils
|
from bandit.core import utils
|
||||||
|
|
||||||
|
|
||||||
@ -37,6 +39,11 @@ class BanditTestSet():
|
|||||||
filter_list = self._filter_list_from_config(profile=profile)
|
filter_list = self._filter_list_from_config(profile=profile)
|
||||||
self.load_tests(filter=filter_list)
|
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):
|
def _filter_list_from_config(self, profile=None):
|
||||||
# will create an (include,exclude) list tuple from a specified name
|
# will create an (include,exclude) list tuple from a specified name
|
||||||
# config section
|
# config section
|
||||||
|
@ -74,7 +74,8 @@ class BanditTester():
|
|||||||
result.lineno = temp_context['lineno']
|
result.lineno = temp_context['lineno']
|
||||||
result.linerange = temp_context['linerange']
|
result.linerange = temp_context['linerange']
|
||||||
result.test = test.__name__
|
result.test = test.__name__
|
||||||
result.test_id = test._test_id
|
if result.test_id == "":
|
||||||
|
result.test_id = test._test_id
|
||||||
|
|
||||||
self.results.append(result)
|
self.results.append(result)
|
||||||
|
|
||||||
|
@ -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)
|
|
@ -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)
|
|
10
setup.cfg
10
setup.cfg
@ -26,6 +26,9 @@ console_scripts =
|
|||||||
bandit = bandit.cli.main:main
|
bandit = bandit.cli.main:main
|
||||||
bandit-config-generator = bandit.cli.config_generator:main
|
bandit-config-generator = bandit.cli.config_generator:main
|
||||||
bandit-baseline = bandit.cli.baseline:main
|
bandit-baseline = bandit.cli.baseline:main
|
||||||
|
bandit.blacklists =
|
||||||
|
calls = bandit.blacklists.calls:gen_blacklist
|
||||||
|
imports = bandit.blacklists.imports:gen_blacklist
|
||||||
bandit.formatters =
|
bandit.formatters =
|
||||||
csv = bandit.formatters.csv:report
|
csv = bandit.formatters.csv:report
|
||||||
json = bandit.formatters.json:report
|
json = bandit.formatters.json:report
|
||||||
@ -40,13 +43,6 @@ bandit.plugins =
|
|||||||
# bandit/plugins/asserts.py
|
# bandit/plugins/asserts.py
|
||||||
assert_used = bandit.plugins.asserts:assert_used
|
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
|
# 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
|
request_with_no_cert_validation = bandit.plugins.crypto_request_no_cert_validation:request_with_no_cert_validation
|
||||||
|
|
||||||
|
@ -116,12 +116,14 @@ class FunctionalTests(testtools.TestCase):
|
|||||||
|
|
||||||
def test_crypto_md5(self):
|
def test_crypto_md5(self):
|
||||||
'''Test the `hashlib.md5` example.'''
|
'''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)
|
self.check_example('crypto-md5.py', expect)
|
||||||
|
|
||||||
def test_ciphers(self):
|
def test_ciphers(self):
|
||||||
'''Test the `Crypto.Cipher` example.'''
|
'''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)
|
self.check_example('ciphers.py', expect)
|
||||||
|
|
||||||
def test_cipher_modes(self):
|
def test_cipher_modes(self):
|
||||||
@ -384,8 +386,8 @@ class FunctionalTests(testtools.TestCase):
|
|||||||
'CONFIDENCE': {'HIGH': 2, 'MEDIUM': 2}}
|
'CONFIDENCE': {'HIGH': 2, 'MEDIUM': 2}}
|
||||||
self.check_example('xml_minidom.py', expect)
|
self.check_example('xml_minidom.py', expect)
|
||||||
|
|
||||||
expect = {'SEVERITY': {'LOW': 1, 'HIGH': 6},
|
expect = {'SEVERITY': {'LOW': 2, 'HIGH': 6},
|
||||||
'CONFIDENCE': {'HIGH': 1, 'MEDIUM': 6}}
|
'CONFIDENCE': {'HIGH': 2, 'MEDIUM': 6}}
|
||||||
self.check_example('xml_sax.py', expect)
|
self.check_example('xml_sax.py', expect)
|
||||||
|
|
||||||
def test_asserts(self):
|
def test_asserts(self):
|
||||||
@ -436,8 +438,8 @@ class FunctionalTests(testtools.TestCase):
|
|||||||
def test_weak_cryptographic_key(self):
|
def test_weak_cryptographic_key(self):
|
||||||
'''Test for weak key sizes.'''
|
'''Test for weak key sizes.'''
|
||||||
expect = {
|
expect = {
|
||||||
'SEVERITY': {'MEDIUM': 5, 'HIGH': 4},
|
'SEVERITY': {'LOW': 2, 'MEDIUM': 5, 'HIGH': 4},
|
||||||
'CONFIDENCE': {'HIGH': 9}
|
'CONFIDENCE': {'HIGH': 11}
|
||||||
}
|
}
|
||||||
self.check_example('weak_cryptographic_key_sizes.py', expect)
|
self.check_example('weak_cryptographic_key_sizes.py', expect)
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ class RuntimeTests(testtools.TestCase):
|
|||||||
self.assertIn("Low: 2", output)
|
self.assertIn("Low: 2", output)
|
||||||
self.assertIn("High: 2", output)
|
self.assertIn("High: 2", output)
|
||||||
self.assertIn("Files skipped (0):", output)
|
self.assertIn("Files skipped (0):", output)
|
||||||
self.assertIn("Issue: [B401:blacklist_imports] Consider possible",
|
self.assertIn("Issue: [B403:blacklist] Consider possible",
|
||||||
output)
|
output)
|
||||||
self.assertIn("imports.py:2", output)
|
self.assertIn("imports.py:2", output)
|
||||||
self.assertIn("imports.py:4", output)
|
self.assertIn("imports.py:4", output)
|
||||||
|
@ -131,7 +131,6 @@ class ManagerTests(testtools.TestCase):
|
|||||||
self.assertEqual(m.debug, False)
|
self.assertEqual(m.debug, False)
|
||||||
self.assertEqual(m.verbose, False)
|
self.assertEqual(m.verbose, False)
|
||||||
self.assertEqual(m.agg_type, 'file')
|
self.assertEqual(m.agg_type, 'file')
|
||||||
self.assertFalse(m.has_tests)
|
|
||||||
|
|
||||||
def test_matches_globlist(self):
|
def test_matches_globlist(self):
|
||||||
self.assertTrue(manager._matches_glob_list('test', ['*tes*']))
|
self.assertTrue(manager._matches_glob_list('test', ['*tes*']))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user