diff options
author | Tristan Cacqueray <tristan.cacqueray@enovance.com> | 2014-09-11 16:31:24 +0000 |
---|---|---|
committer | Tristan Cacqueray <tristan.cacqueray@enovance.com> | 2014-09-12 17:28:54 +0000 |
commit | 4cf1a2a158f7c3c21799bf2e6ba0e7b5fbc25d62 (patch) | |
tree | e776e84897085f59a0d0d232bdd6864d7fb9a56e | |
parent | 889bdf712ec25e4afd21ce42b6271e0638bb0607 (diff) | |
download | oslo-incubator-stable/havana.tar.gz |
Fix password leak from ProcessExecution errorshavana-eolstable/havana
This backport required changes to fix both issues:
* Make execute method clean password in exception
* Make sure mask_password works properly
This backport is not trivial as these fixes relies on many other
changes, only the necessary code have been imported/adapted.
------------------------------------------------
The sync pulls in the following changes (newest to oldest):
63c99a0f - Mask passwords in exceptions and error messages
66142c34 - Make strutils.mask_password more secure
d6b55fb2 - Remove `processutils` dependency on `log`
cb5a804b - Move `mask_password` to strutils
-----------------------------------------------
Closes-Bug: 1343604
Closes-Bug: 1345233
SecurityImpact
Change-Id: I3b49b1d667f6ade9ae3f6765d735440a3e838917
-rw-r--r-- | openstack/common/processutils.py | 24 | ||||
-rw-r--r-- | openstack/common/strutils.py | 72 | ||||
-rw-r--r-- | tests/unit/test_processutils.py | 33 | ||||
-rw-r--r-- | tests/unit/test_strutils.py | 287 |
4 files changed, 405 insertions, 11 deletions
diff --git a/openstack/common/processutils.py b/openstack/common/processutils.py index 9364559f..a936176a 100644 --- a/openstack/common/processutils.py +++ b/openstack/common/processutils.py @@ -19,7 +19,7 @@ System-level utilities and helper functions. """ -import logging as stdlib_logging +import logging import os import random import shlex @@ -29,7 +29,7 @@ from eventlet.green import subprocess from eventlet import greenthread from openstack.common.gettextutils import _ # noqa -from openstack.common import log as logging +from openstack.common import strutils LOG = logging.getLogger(__name__) @@ -104,8 +104,7 @@ def execute(*cmd, **kwargs): execute this command. Defaults to false. :type shell: boolean :param loglevel: log level for execute commands. - :type loglevel: int. (Should be stdlib_logging.DEBUG or - stdlib_logging.INFO) + :type loglevel: int. (Should be logging.DEBUG or logging.INFO) :returns: (stdout, stderr) from process execution :raises: :class:`UnknownArgumentError` on receiving unknown arguments @@ -120,7 +119,7 @@ def execute(*cmd, **kwargs): run_as_root = kwargs.pop('run_as_root', False) root_helper = kwargs.pop('root_helper', '') shell = kwargs.pop('shell', False) - loglevel = kwargs.pop('loglevel', stdlib_logging.DEBUG) + loglevel = kwargs.pop('loglevel', logging.DEBUG) if isinstance(check_exit_code, bool): ignore_exit_code = not check_exit_code @@ -140,11 +139,12 @@ def execute(*cmd, **kwargs): cmd = shlex.split(root_helper) + list(cmd) cmd = map(str, cmd) + sanitized_cmd = strutils.mask_password(' '.join(cmd)) while attempts > 0: attempts -= 1 try: - LOG.log(loglevel, _('Running cmd (subprocess): %s'), ' '.join(cmd)) + LOG.log(loglevel, _('Running cmd (subprocess): %s'), sanitized_cmd) _PIPE = subprocess.PIPE # pylint: disable=E1101 if os.name == 'nt': @@ -172,16 +172,18 @@ def execute(*cmd, **kwargs): LOG.log(loglevel, _('Result was %s') % _returncode) if not ignore_exit_code and _returncode not in check_exit_code: (stdout, stderr) = result + sanitized_stdout = strutils.mask_password(stdout) + sanitized_stderr = strutils.mask_password(stderr) raise ProcessExecutionError(exit_code=_returncode, - stdout=stdout, - stderr=stderr, - cmd=' '.join(cmd)) + stdout=sanitized_stdout, + stderr=sanitized_stderr, + cmd=sanitized_cmd) return result except ProcessExecutionError: if not attempts: raise else: - LOG.log(loglevel, _('%r failed. Retrying.'), cmd) + LOG.log(loglevel, _('%r failed. Retrying.'), sanitized_cmd) if delay_on_retry: greenthread.sleep(random.randint(20, 200) / 100.0) finally: @@ -220,7 +222,7 @@ def trycmd(*args, **kwargs): def ssh_execute(ssh, cmd, process_input=None, addl_env=None, check_exit_code=True): - LOG.debug(_('Running cmd (SSH): %s'), cmd) + LOG.debug('Running cmd (SSH): %s', cmd) if addl_env: raise InvalidArgumentError(_('Environment not supported over SSH')) diff --git a/openstack/common/strutils.py b/openstack/common/strutils.py index 971ffe3c..38b0f615 100644 --- a/openstack/common/strutils.py +++ b/openstack/common/strutils.py @@ -46,6 +46,39 @@ SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]") SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+") +# NOTE(flaper87): The following globals are used by `mask_password` +_SANITIZE_KEYS = ['adminPass', 'admin_pass', 'password', 'admin_password'] + +# NOTE(ldbragst): Let's build a list of regex objects using the list of +# _SANITIZE_KEYS we already have. This way, we only have to add the new key +# to the list of _SANITIZE_KEYS and we can generate regular expressions +# for XML and JSON automatically. +_SANITIZE_PATTERNS_2 = [] +_SANITIZE_PATTERNS_1 = [] + +# NOTE(amrith): Some regular expressions have only one parameter, some +# have two parameters. Use different lists of patterns here. +_FORMAT_PATTERNS_1 = [r'(%(key)s\s*[=]\s*)[^\s^\'^\"]+'] +_FORMAT_PATTERNS_2 = [r'(%(key)s\s*[=]\s*[\"\']).*?([\"\'])', + r'(%(key)s\s+[\"\']).*?([\"\'])', + r'([-]{2}%(key)s\s+)[^\'^\"^=^\s]+([\s]*)', + r'(<%(key)s>).*?(</%(key)s>)', + r'([\"\']%(key)s[\"\']\s*:\s*[\"\']).*?([\"\'])', + r'([\'"].*?%(key)s[\'"]\s*:\s*u?[\'"]).*?([\'"])', + r'([\'"].*?%(key)s[\'"]\s*,\s*\'--?[A-z]+\'\s*,\s*u?' + '[\'"]).*?([\'"])', + r'(%(key)s\s*--?[A-z]+\s*)\S+(\s*)'] + +for key in _SANITIZE_KEYS: + for pattern in _FORMAT_PATTERNS_2: + reg_ex = re.compile(pattern % {'key': key}, re.DOTALL) + _SANITIZE_PATTERNS_2.append(reg_ex) + + for pattern in _FORMAT_PATTERNS_1: + reg_ex = re.compile(pattern % {'key': key}, re.DOTALL) + _SANITIZE_PATTERNS_1.append(reg_ex) + + def int_from_bool_as_string(subject): """Interpret a string as a boolean and return either 1 or 0. @@ -216,3 +249,42 @@ def to_slug(value, incoming=None, errors="strict"): "ascii", "ignore").decode("ascii") value = SLUGIFY_STRIP_RE.sub("", value).strip().lower() return SLUGIFY_HYPHENATE_RE.sub("-", value) + + +def mask_password(message, secret="***"): + """Replace password with 'secret' in message. + + :param message: The string which includes security information. + :param secret: value with which to replace passwords. + :returns: The unicode value of message with the password fields masked. + + For example: + + >>> mask_password("'adminPass' : 'aaaaa'") + "'adminPass' : '***'" + >>> mask_password("'admin_pass' : 'aaaaa'") + "'admin_pass' : '***'" + >>> mask_password('"password" : "aaaaa"') + '"password" : "***"' + >>> mask_password("'original_password' : 'aaaaa'") + "'original_password' : '***'" + >>> mask_password("u'original_password' : u'aaaaa'") + "u'original_password' : u'***'" + """ + message = six.text_type(message) + + # NOTE(ldbragst): Check to see if anything in message contains any key + # specified in _SANITIZE_KEYS, if not then just return the message since + # we don't have to mask any passwords. + if not any(key in message for key in _SANITIZE_KEYS): + return message + + substitute = r'\g<1>' + secret + r'\g<2>' + for pattern in _SANITIZE_PATTERNS_2: + message = re.sub(pattern, substitute, message) + + substitute = r'\g<1>' + secret + for pattern in _SANITIZE_PATTERNS_1: + message = re.sub(pattern, substitute, message) + + return message diff --git a/tests/unit/test_processutils.py b/tests/unit/test_processutils.py index 0f9289c5..0eeecf4d 100644 --- a/tests/unit/test_processutils.py +++ b/tests/unit/test_processutils.py @@ -19,6 +19,7 @@ from __future__ import print_function import fixtures import os +import stat import tempfile import six @@ -26,6 +27,14 @@ import six from openstack.common import processutils from openstack.common import test +TEST_EXCEPTION_AND_MASKING_SCRIPT = """#!/bin/bash +# This is to test stdout and stderr +# and the command returned in an exception +# when a non-zero exit code is returned +echo onstdout --password='"secret"' +echo onstderr --password='"secret"' 1>&2 +exit 38""" + class UtilsTest(test.BaseTestCase): # NOTE(jkoelker) Moar tests from nova need to be ported. But they @@ -217,6 +226,30 @@ class FakeSshStream(six.StringIO): def setup_channel(self, rc): self.channel = FakeSshChannel(rc) + def test_exception_and_masking(self): + tmpfilename = self.create_tempfiles( + [["test_exceptions_and_masking", + TEST_EXCEPTION_AND_MASKING_SCRIPT]], ext='bash')[0] + + os.chmod(tmpfilename, (stat.S_IRWXU | + stat.S_IRGRP | + stat.S_IXGRP | + stat.S_IROTH | + stat.S_IXOTH)) + + err = self.assertRaises(processutils.ProcessExecutionError, + processutils.execute, + tmpfilename, 'password="secret"', + 'something') + + self.assertEqual(38, err.exit_code) + self.assertEqual(err.stdout, 'onstdout --password="***"\n') + self.assertEqual(err.stderr, 'onstderr --password="***"\n') + self.assertEqual(err.cmd, ' '.join([tmpfilename, + 'password="***"', + 'something'])) + self.assertNotIn('secret', str(err)) + class FakeSshConnection(object): def __init__(self, rc): diff --git a/tests/unit/test_strutils.py b/tests/unit/test_strutils.py index 22aafd9a..302f55b8 100644 --- a/tests/unit/test_strutils.py +++ b/tests/unit/test_strutils.py @@ -213,3 +213,290 @@ class StrUtilsTest(test.BaseTestCase): self.assertEqual(six.u("perche"), to_slug("perch\xc3\xa9")) self.assertEqual(six.u("strange"), to_slug("\x80strange", errors="ignore")) + + +class MaskPasswordTestCase(test.BaseTestCase): + + def test_json(self): + # Test 'adminPass' w/o spaces + payload = """{'adminPass':'mypassword'}""" + expected = """{'adminPass':'***'}""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'adminPass' with spaces + payload = """{ 'adminPass' : 'mypassword' }""" + expected = """{ 'adminPass' : '***' }""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'admin_pass' w/o spaces + payload = """{'admin_pass':'mypassword'}""" + expected = """{'admin_pass':'***'}""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'admin_pass' with spaces + payload = """{ 'admin_pass' : 'mypassword' }""" + expected = """{ 'admin_pass' : '***' }""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'admin_password' w/o spaces + payload = """{'admin_password':'mypassword'}""" + expected = """{'admin_password':'***'}""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'admin_password' with spaces + payload = """{ 'admin_password' : 'mypassword' }""" + expected = """{ 'admin_password' : '***' }""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'password' w/o spaces + payload = """{'password':'mypassword'}""" + expected = """{'password':'***'}""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'password' with spaces + payload = """{ 'password' : 'mypassword' }""" + expected = """{ 'password' : '***' }""" + self.assertEqual(expected, strutils.mask_password(payload)) + + def test_xml(self): + # Test 'adminPass' w/o spaces + payload = """<adminPass>mypassword</adminPass>""" + expected = """<adminPass>***</adminPass>""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'adminPass' with spaces + payload = """<adminPass> + mypassword + </adminPass>""" + expected = """<adminPass>***</adminPass>""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'admin_pass' w/o spaces + payload = """<admin_pass>mypassword</admin_pass>""" + expected = """<admin_pass>***</admin_pass>""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'admin_pass' with spaces + payload = """<admin_pass> + mypassword + </admin_pass>""" + expected = """<admin_pass>***</admin_pass>""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'admin_password' w/o spaces + payload = """<admin_password>mypassword</admin_password>""" + expected = """<admin_password>***</admin_password>""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'admin_password' with spaces + payload = """<admin_password> + mypassword + </admin_password>""" + expected = """<admin_password>***</admin_password>""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'password' w/o spaces + payload = """<password>mypassword</password>""" + expected = """<password>***</password>""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'password' with spaces + payload = """<password> + mypassword + </password>""" + expected = """<password>***</password>""" + self.assertEqual(expected, strutils.mask_password(payload)) + + def test_xml_attribute(self): + # Test 'adminPass' w/o spaces + payload = """adminPass='mypassword'""" + expected = """adminPass='***'""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'adminPass' with spaces + payload = """adminPass = 'mypassword'""" + expected = """adminPass = '***'""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'adminPass' with double quotes + payload = """adminPass = "mypassword\"""" + expected = """adminPass = "***\"""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'admin_pass' w/o spaces + payload = """admin_pass='mypassword'""" + expected = """admin_pass='***'""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'admin_pass' with spaces + payload = """admin_pass = 'mypassword'""" + expected = """admin_pass = '***'""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'admin_pass' with double quotes + payload = """admin_pass = "mypassword\"""" + expected = """admin_pass = "***\"""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'admin_password' w/o spaces + payload = """admin_password='mypassword'""" + expected = """admin_password='***'""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'admin_password' with spaces + payload = """admin_password = 'mypassword'""" + expected = """admin_password = '***'""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'admin_password' with double quotes + payload = """admin_password = "mypassword\"""" + expected = """admin_password = "***\"""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'password' w/o spaces + payload = """password='mypassword'""" + expected = """password='***'""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'password' with spaces + payload = """password = 'mypassword'""" + expected = """password = '***'""" + self.assertEqual(expected, strutils.mask_password(payload)) + # Test 'password' with double quotes + payload = """password = "mypassword\"""" + expected = """password = "***\"""" + self.assertEqual(expected, strutils.mask_password(payload)) + + def test_json_message(self): + payload = """body: {"changePassword": {"adminPass": "1234567"}}""" + expected = """body: {"changePassword": {"adminPass": "***"}}""" + self.assertEqual(expected, strutils.mask_password(payload)) + payload = """body: {"rescue": {"admin_pass": "1234567"}}""" + expected = """body: {"rescue": {"admin_pass": "***"}}""" + self.assertEqual(expected, strutils.mask_password(payload)) + payload = """body: {"rescue": {"admin_password": "1234567"}}""" + expected = """body: {"rescue": {"admin_password": "***"}}""" + self.assertEqual(expected, strutils.mask_password(payload)) + payload = """body: {"rescue": {"password": "1234567"}}""" + expected = """body: {"rescue": {"password": "***"}}""" + self.assertEqual(expected, strutils.mask_password(payload)) + + def test_xml_message(self): + payload = """<?xml version="1.0" encoding="UTF-8"?> +<rebuild + xmlns="http://docs.openstack.org/compute/api/v1.1" + name="foobar" + imageRef="http://openstack.example.com/v1.1/32278/images/70a599e0-31e7" + accessIPv4="1.2.3.4" + accessIPv6="fe80::100" + adminPass="seekr3t"> + <metadata> + <meta key="My Server Name">Apache1</meta> + </metadata> +</rebuild>""" + expected = """<?xml version="1.0" encoding="UTF-8"?> +<rebuild + xmlns="http://docs.openstack.org/compute/api/v1.1" + name="foobar" + imageRef="http://openstack.example.com/v1.1/32278/images/70a599e0-31e7" + accessIPv4="1.2.3.4" + accessIPv6="fe80::100" + adminPass="***"> + <metadata> + <meta key="My Server Name">Apache1</meta> + </metadata> +</rebuild>""" + self.assertEqual(expected, strutils.mask_password(payload)) + payload = """<?xml version="1.0" encoding="UTF-8"?> +<rescue xmlns="http://docs.openstack.org/compute/api/v1.1" + admin_pass="MySecretPass"/>""" + expected = """<?xml version="1.0" encoding="UTF-8"?> +<rescue xmlns="http://docs.openstack.org/compute/api/v1.1" + admin_pass="***"/>""" + self.assertEqual(expected, strutils.mask_password(payload)) + payload = """<?xml version="1.0" encoding="UTF-8"?> +<rescue xmlns="http://docs.openstack.org/compute/api/v1.1" + admin_password="MySecretPass"/>""" + expected = """<?xml version="1.0" encoding="UTF-8"?> +<rescue xmlns="http://docs.openstack.org/compute/api/v1.1" + admin_password="***"/>""" + self.assertEqual(expected, strutils.mask_password(payload)) + payload = """<?xml version="1.0" encoding="UTF-8"?> +<rescue xmlns="http://docs.openstack.org/compute/api/v1.1" + password="MySecretPass"/>""" + expected = """<?xml version="1.0" encoding="UTF-8"?> +<rescue xmlns="http://docs.openstack.org/compute/api/v1.1" + password="***"/>""" + self.assertEqual(expected, strutils.mask_password(payload)) + + def test_mask_password(self): + payload = "test = 'password' : 'aaaaaa'" + expected = "test = 'password' : '111'" + self.assertEqual(expected, + strutils.mask_password(payload, secret='111')) + + payload = 'mysqld --password "aaaaaa"' + expected = 'mysqld --password "****"' + self.assertEqual(expected, + strutils.mask_password(payload, secret='****')) + + payload = 'mysqld --password aaaaaa' + expected = 'mysqld --password ???' + self.assertEqual(expected, + strutils.mask_password(payload, secret='???')) + + payload = 'mysqld --password = "aaaaaa"' + expected = 'mysqld --password = "****"' + self.assertEqual(expected, + strutils.mask_password(payload, secret='****')) + + payload = "mysqld --password = 'aaaaaa'" + expected = "mysqld --password = '****'" + self.assertEqual(expected, + strutils.mask_password(payload, secret='****')) + + payload = "mysqld --password = aaaaaa" + expected = "mysqld --password = ****" + self.assertEqual(expected, + strutils.mask_password(payload, secret='****')) + + payload = "test = password = aaaaaa" + expected = "test = password = 111" + self.assertEqual(expected, + strutils.mask_password(payload, secret='111')) + + payload = "test = password= aaaaaa" + expected = "test = password= 111" + self.assertEqual(expected, + strutils.mask_password(payload, secret='111')) + + payload = "test = password =aaaaaa" + expected = "test = password =111" + self.assertEqual(expected, + strutils.mask_password(payload, secret='111')) + + payload = "test = password=aaaaaa" + expected = "test = password=111" + self.assertEqual(expected, + strutils.mask_password(payload, secret='111')) + + payload = 'test = "original_password" : "aaaaaaaaa"' + expected = 'test = "original_password" : "***"' + self.assertEqual(expected, strutils.mask_password(payload)) + + payload = 'test = "param1" : "value"' + expected = 'test = "param1" : "value"' + self.assertEqual(expected, strutils.mask_password(payload)) + + payload = """{'adminPass':'mypassword'}""" + payload = six.text_type(payload) + expected = """{'adminPass':'***'}""" + self.assertEqual(expected, strutils.mask_password(payload)) + + payload = ("test = 'node.session.auth.password','-v','mypassword'," + "'nomask'") + expected = ("test = 'node.session.auth.password','-v','***'," + "'nomask'") + self.assertEqual(expected, strutils.mask_password(payload)) + + payload = ("test = 'node.session.auth.password', '--password', " + "'mypassword', 'nomask'") + expected = ("test = 'node.session.auth.password', '--password', " + "'***', 'nomask'") + self.assertEqual(expected, strutils.mask_password(payload)) + + payload = ("test = 'node.session.auth.password', '--password', " + "'mypassword'") + expected = ("test = 'node.session.auth.password', '--password', " + "'***'") + self.assertEqual(expected, strutils.mask_password(payload)) + + payload = "test = node.session.auth.password -v mypassword nomask" + expected = "test = node.session.auth.password -v *** nomask" + self.assertEqual(expected, strutils.mask_password(payload)) + + payload = ("test = node.session.auth.password --password mypassword " + "nomask") + expected = ("test = node.session.auth.password --password *** " + "nomask") + self.assertEqual(expected, strutils.mask_password(payload)) + + payload = ("test = node.session.auth.password --password mypassword") + expected = ("test = node.session.auth.password --password ***") + self.assertEqual(expected, strutils.mask_password(payload)) |