Improved tests for hardcoded passwords

This replaces the existing hardcoded password test with a number of
smarter tests. None of the new tests utilize a word dictionary, we
now trigger the warnings based on matching variable names and the
like against a list of candidate names:

 - "password"
 - "pass"
 - "passwd"
 - "pwd"
 - "secret"
 - "token"

hardcoded_password_string looks for:
 candidate = "some_string_literal"
 dict[candidate] = "some_string_literal"
 candidate == "some_string_literal"

hardcoded_password_funcarg looks for:
 func_call(candidate="some_string_literal")

hardcoded_password_default looks for:
 def func_def(candidate="some_string_literal"):

All issues are reported as MEDIUM confidence, LOW severity

Closes-bug: #1502348
Closes-bug: #1502343
Closes-bug: #1432887

Change-Id: I36d97ee838a7f08234b759c352649721d07e8ab0
This commit is contained in:
Tim Kelsey 2015-10-02 12:41:00 +01:00
parent b7d41d57b7
commit 604ca79759
5 changed files with 62 additions and 63 deletions

View File

@ -14,72 +14,67 @@
# License for the specific language governing permissions and limitations
# under the License.
import os.path
import warnings
from appdirs import site_data_dir
import sys
import bandit
from bandit.core.test_properties import *
_wordlist = []
candidates = set(["password", "pass", "passwd", "pwd", "secret", "token",
"secrete"])
def find_word_list(cfg_word_list_f):
if not isinstance(cfg_word_list_f, str):
return None
try:
cfg_word_list_f % {'site_data_dir': ''}
except TypeError:
return cfg_word_list_f
site_data_dirs = ['.'] + site_data_dir("bandit", "",
multipath=True).split(':')
for dir in site_data_dirs:
word_list_path = cfg_word_list_f % {'site_data_dir': dir}
if os.path.isfile(word_list_path):
if dir == ".":
warnings.warn("Using relative path for word_list: %s"
% word_list_path)
return word_list_path
raise RuntimeError("Could not substitute '%(site_data_dir)s' "
"to a path with a valid word_list file")
def _report(value):
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.MEDIUM,
text=("Possible hardcoded password: '%s'" % value))
def _get_wordlist(config):
global _wordlist
if not len(_wordlist):
# try to read the word list file from config
if (config is not None and 'word_list' in config):
try:
word_list_file = find_word_list(config['word_list'])
except RuntimeError as e:
warnings.warn(e.message)
return
# try to open the word list file and read passwords from it
try:
f = open(word_list_file, 'r')
except (OSError, IOError):
raise RuntimeError("Could not open word_list (from config"
" file): %s" % word_list_file)
else:
_wordlist = [word.strip() for word in f]
f.close()
return _wordlist
@takes_config
@checks('Str')
def hardcoded_password(context, config):
word_list = _get_wordlist(config)
def hardcoded_password_string(context):
node = context.node
if isinstance(node.parent, ast.Assign):
# looks for "candidate='some_string'"
for targ in node.parent.targets:
if isinstance(targ, ast.Name) and targ.id in candidates:
return _report(node.s)
# for every password in the list, check against the current string
for word in word_list:
if context.string_val and context.string_val == word:
return bandit.Issue(
severity=bandit.LOW,
confidence=bandit.LOW,
text="Possible hardcoded password '(%s)'" % word
)
elif isinstance(node.parent, ast.Index) and node.s in candidates:
# looks for "dict[candidate]='some_string'"
# assign -> subscript -> index -> string
assign = node.parent.parent.parent
if isinstance(assign, ast.Assign) and isinstance(assign.value,
ast.Str):
return _report(assign.value.s)
elif isinstance(node.parent, ast.Compare):
# looks for "candidate == 'some_string'"
comp = node.parent
if isinstance(comp.left, ast.Name) and comp.left.id in candidates:
if isinstance(comp.comparators[0], ast.Str):
return _report(comp.comparators[0].s)
@checks('Call')
def hardcoded_password_funcarg(context):
# looks for "function(candidate='some_string')"
for kw in context.node.keywords:
if isinstance(kw.value, ast.Str) and kw.arg in candidates:
return _report(kw.value.s)
@checks('FunctionDef')
def hardcoded_password_default(context):
# looks for "def function(candidate='some_string')"
# this pads the list of default values with "None" if nothing is given
defs = [None] * (len(context.node.args.args) -
len(context.node.args.defaults))
defs.extend(context.node.args.defaults)
# go through all (param, value)s and look for candidates
for key, val in zip(context.node.args.args, defs):
check = key.arg if sys.version_info.major > 2 else key.id # Py3
if isinstance(val, ast.Str) and check in candidates:
return _report(val.s)

View File

@ -13,3 +13,6 @@ def NoMatch2(password):
if password == "ajklawejrkl42348swfgkg":
print("Nice password!")
doLogin(password="blerg")
password = "blerg"
d["password"] = "blerg"

View File

@ -26,8 +26,7 @@ def test_urlopen():
handler = urllib2.HTTPBasicAuthHandler()
handler.add_password(realm='test',
uri='http://mysite.com',
user='bob',
passwd='blah')
user='bob')
opener = urllib2.build_opener(handler)
urllib2.install_opener(opener)
urllib2.urlopen('file:///bin/ls')

View File

@ -57,7 +57,9 @@ bandit.plugins =
hardcoded_bind_all_interfaces = bandit.plugins.general_bind_all_interfaces:hardcoded_bind_all_interfaces
# bandit/plugins/general_hardcoded_password.py
hardcoded_password = bandit.plugins.general_hardcoded_password:hardcoded_password
hardcoded_password_string = bandit.plugins.general_hardcoded_password:hardcoded_password_string
hardcoded_password_funcarg = bandit.plugins.general_hardcoded_password:hardcoded_password_funcarg
hardcoded_password_default = bandit.plugins.general_hardcoded_password:hardcoded_password_default
# bandit/plugins/general_hardcoded_tmp.py
hardcoded_tmp_directory = bandit.plugins.general_hardcoded_tmp:hardcoded_tmp_directory

View File

@ -120,7 +120,7 @@ class FunctionalTests(testtools.TestCase):
def test_hardcoded_passwords(self):
'''Test for hard-coded passwords.'''
expect = {'SEVERITY': {'LOW': 2}, 'CONFIDENCE': {'LOW': 2}}
expect = {'SEVERITY': {'LOW': 7}, 'CONFIDENCE': {'MEDIUM': 7}}
self.check_example('hardcoded-passwords.py', expect)
def test_hardcoded_tmp(self):