CVE-2012-3361 (Python, published 2012-07-22T16:55Z)
Description: value: virt/disk/api.py in OpenStack Compute (Nova) Folsom (2012.2), Essex (2012.1), and Diablo (2011.3) allows remote authenticated users to overwrite arbitrary files via a symlink attack on a file in an image.
Severity: MEDIUM (CVSS v3 base score: )
Weakness: CWE-264 \u2014 Permissions, Privileges, and Access Controls
Weaknesses in this category are related to the management of permissions, privileges, and other security features that are used to perform access control.
Repository: https://github.com/openstack/nova
Files changed:
nova/tests/test_virt.py
nova/virt/disk/api.py
Commit message: Prevent file injection writing to host filesystem.

Fix bug 1015531, CVE-2012-3360, CVE-2012-3361

This patch prevents the file injection code from writing into the host
filesystem if a user specifies a path for the injected file that
contains '..'.  The check is to make sure that the final normalized path
that is about to be written to is within the mounted guest filesystem.

Signed-off-by: Russell Bryant <rbryant@redhat.com>
Signed-off-by: Pádraig Brady <pbrady@redhat.com>
Signed-off-by: Mark McLoughlin <markmc@redhat.com>

Change-Id: I658cd12fd319cee91eb9544cdf53c862c5d2c560


Vulnerable code:
def _inject_file_into_fs(fs, path, contents):
    absolute_path = os.path.join(fs, path.lstrip('/'))
    utils.execute('tee', absolute_path, process_input=contents,
          run_as_root=True)
    metadata_path = os.path.join(fs, "meta.js")

    utils.execute('tee', metadata_path,
                  process_input=jsonutils.dumps(metadata), run_as_root=True)
    sshdir = os.path.join(fs, 'root', '.ssh')
    keyfile = os.path.join(sshdir, 'authorized_keys')
    key_data = [
    ]
    utils.execute('tee', '-a', keyfile,
                  process_input=''.join(key_data), run_as_root=True)
    netdir = os.path.join(os.path.join(fs, 'etc'), 'network')
    netfile = os.path.join(netdir, 'interfaces')
    utils.execute('tee', netfile, process_input=net, run_as_root=True)
    utils.execute('cp', os.path.join(fs, 'etc', 'passwd'), tmp_passwd,
                  run_as_root=True)
    utils.execute('cp', os.path.join(fs, 'etc', 'shadow'), tmp_shadow,
                  run_as_root=True)
    utils.execute('cp', tmp_passwd, os.path.join(fs, 'etc', 'passwd'),
                  run_as_root=True)
    utils.execute('cp', tmp_shadow, os.path.join(fs, 'etc', 'shadow'),
                  run_as_root=True)

Fixed code:
from nova import exception
from nova.virt.disk import api as disk_api


class TestVirtDisk(test.TestCase):
    def test_check_safe_path(self):
        ret = disk_api._join_and_check_path_within_fs('/foo', 'etc',
                                                      'something.conf')
        self.assertEquals(ret, '/foo/etc/something.conf')

    def test_check_unsafe_path(self):
        self.assertRaises(exception.Invalid,
                          disk_api._join_and_check_path_within_fs,
                          '/foo', 'etc/../../../something.conf')

    def test_inject_files_with_bad_path(self):
        self.assertRaises(exception.Invalid,
                          disk_api._inject_file_into_fs,
                          '/tmp', '/etc/../../../../etc/passwd',
                          'hax')
def _join_and_check_path_within_fs(fs, *args):
    '''os.path.join() with safety check for injected file paths.

    Join the supplied path components and make sure that the
    resulting path we are injecting into is within the
    mounted guest fs.  Trying to be clever and specifying a
    path with '..' in it will hit this safeguard.
    '''
    absolute_path = os.path.realpath(os.path.join(fs, *args))
    if not absolute_path.startswith(os.path.realpath(fs) + '/'):
        raise exception.Invalid(_('injected file path not valid'))
    return absolute_path


def _inject_file_into_fs(fs, path, contents, append=False):
    absolute_path = _join_and_check_path_within_fs(fs, path.lstrip('/'))


    args = []
    if append:
        args.append('-a')
    args.append(absolute_path)

    kwargs = dict(process_input=contents, run_as_root=True)

    utils.execute('tee', *args, **kwargs)
    _inject_file_into_fs(fs, 'meta.js', jsonutils.dumps(metadata))
    sshdir = _join_and_check_path_within_fs(fs, 'root', '.ssh')

    keyfile = os.path.join('root', '.ssh', 'authorized_keys')

    key_data = ''.join([
    ])

    _inject_file_into_fs(fs, keyfile, key_data, append=True)
    netdir = _join_and_check_path_within_fs(fs, 'etc', 'network')

    netfile = os.path.join('etc', 'network', 'interfaces')
    _inject_file_into_fs(fs, netfile, net)
    passwd_path = _join_and_check_path_within_fs(fs, 'etc', 'passwd')
    shadow_path = _join_and_check_path_within_fs(fs, 'etc', 'shadow')

    utils.execute('cp', passwd_path, tmp_passwd, run_as_root=True)
    utils.execute('cp', shadow_path, tmp_shadow, run_as_root=True)
    utils.execute('cp', tmp_passwd, passwd_path, run_as_root=True)
    utils.execute('cp', tmp_shadow, shadow_path, run_as_root=True)