Moderate severityNVD Advisory· Published Jul 22, 2012· Updated Apr 29, 2026
CVE-2012-3360
CVE-2012-3360
Description
Directory traversal vulnerability in virt/disk/api.py in OpenStack Compute (Nova) Folsom (2012.2) and Essex (2012.1), when used over libvirt-based hypervisors, allows remote authenticated users to write arbitrary files to the disk image via a .. (dot dot) in the path attribute of a file element.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
novaPyPI | < 12.0.0a0 | 12.0.0a0 |
Affected products
2Patches
2b0feaffdb2b1Prevent file injection writing to host filesystem.
2 files changed · +66 −25
nova/tests/test_virt.py+20 −0 modified@@ -15,8 +15,10 @@ # License for the specific language governing permissions and limitations # under the License. +from nova import exception from nova import flags from nova import test +from nova.virt.disk import api as disk_api from nova.virt import driver FLAGS = flags.FLAGS @@ -81,3 +83,21 @@ def test_swap_is_usable(self): 'swap_size': 0})) self.assertTrue(driver.swap_is_usable({'device_name': '/dev/sdb', 'swap_size': 1})) + + +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')
nova/virt/disk/api.py+46 −25 modified@@ -306,20 +306,39 @@ def inject_data_into_fs(fs, key, net, metadata, admin_password, execute): _inject_admin_password_into_fs(admin_password, fs, execute=execute) -def _inject_file_into_fs(fs, path, contents): - absolute_path = os.path.join(fs, path.lstrip('/')) +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('/')) + parent_dir = os.path.dirname(absolute_path) utils.execute('mkdir', '-p', parent_dir, run_as_root=True) - utils.execute('tee', absolute_path, process_input=contents, - run_as_root=True) + + args = [] + if append: + args.append('-a') + args.append(absolute_path) + + kwargs = dict(process_input=contents, run_as_root=True) + + utils.execute('tee', *args, **kwargs) def _inject_metadata_into_fs(metadata, fs, execute=None): - metadata_path = os.path.join(fs, "meta.js") metadata = dict([(m.key, m.value) for m in metadata]) - - utils.execute('tee', metadata_path, - process_input=json.dumps(metadata), run_as_root=True) + _inject_file_into_fs(fs, 'meta.js', json.dumps(metadata)) def _inject_key_into_fs(key, fs, execute=None): @@ -328,33 +347,36 @@ def _inject_key_into_fs(key, fs, execute=None): key is an ssh key string. fs is the path to the base of the filesystem into which to inject the key. """ - sshdir = os.path.join(fs, 'root', '.ssh') + sshdir = _join_and_check_path_within_fs(fs, 'root', '.ssh') utils.execute('mkdir', '-p', sshdir, run_as_root=True) utils.execute('chown', 'root', sshdir, run_as_root=True) utils.execute('chmod', '700', sshdir, run_as_root=True) - keyfile = os.path.join(sshdir, 'authorized_keys') - key_data = [ + + keyfile = os.path.join('root', '.ssh', 'authorized_keys') + + key_data = ''.join([ '\n', '# The following ssh key was injected by Nova', '\n', key.strip(), '\n', - ] - utils.execute('tee', '-a', keyfile, - process_input=''.join(key_data), run_as_root=True) + ]) + + _inject_file_into_fs(fs, keyfile, key_data, append=True) def _inject_net_into_fs(net, fs, execute=None): """Inject /etc/network/interfaces into the filesystem rooted at fs. net is the contents of /etc/network/interfaces. """ - netdir = os.path.join(os.path.join(fs, 'etc'), 'network') + netdir = _join_and_check_path_within_fs(fs, 'etc', 'network') utils.execute('mkdir', '-p', netdir, run_as_root=True) utils.execute('chown', 'root:root', netdir, run_as_root=True) utils.execute('chmod', 755, netdir, run_as_root=True) - netfile = os.path.join(netdir, 'interfaces') - utils.execute('tee', netfile, process_input=net, run_as_root=True) + + netfile = os.path.join('etc', 'network', 'interfaces') + _inject_file_into_fs(fs, netfile, net) def _inject_admin_password_into_fs(admin_passwd, fs, execute=None): @@ -379,16 +401,15 @@ def _inject_admin_password_into_fs(admin_passwd, fs, execute=None): fd, tmp_shadow = tempfile.mkstemp() os.close(fd) - 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) + 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) _set_passwd(admin_user, admin_passwd, tmp_passwd, tmp_shadow) - utils.execute('cp', tmp_passwd, os.path.join(fs, 'etc', 'passwd'), - run_as_root=True) + utils.execute('cp', tmp_passwd, passwd_path, run_as_root=True) os.unlink(tmp_passwd) - utils.execute('cp', tmp_shadow, os.path.join(fs, 'etc', 'shadow'), - run_as_root=True) + utils.execute('cp', tmp_shadow, shadow_path, run_as_root=True) os.unlink(tmp_shadow)
2427d4a99bedPrevent file injection writing to host filesystem.
2 files changed · +66 −25
nova/tests/test_virt.py+20 −0 modified@@ -15,8 +15,10 @@ # License for the specific language governing permissions and limitations # under the License. +from nova import exception from nova import flags from nova import test +from nova.virt.disk import api as disk_api from nova.virt import driver FLAGS = flags.FLAGS @@ -81,3 +83,21 @@ def test_swap_is_usable(self): 'swap_size': 0})) self.assertTrue(driver.swap_is_usable({'device_name': '/dev/sdb', 'swap_size': 1})) + + +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')
nova/virt/disk/api.py+46 −25 modified@@ -311,20 +311,39 @@ def inject_data_into_fs(fs, key, net, metadata, admin_password, execute): _inject_admin_password_into_fs(admin_password, fs, execute=execute) -def _inject_file_into_fs(fs, path, contents): - absolute_path = os.path.join(fs, path.lstrip('/')) +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('/')) + parent_dir = os.path.dirname(absolute_path) utils.execute('mkdir', '-p', parent_dir, run_as_root=True) - utils.execute('tee', absolute_path, process_input=contents, - run_as_root=True) + + args = [] + if append: + args.append('-a') + args.append(absolute_path) + + kwargs = dict(process_input=contents, run_as_root=True) + + utils.execute('tee', *args, **kwargs) def _inject_metadata_into_fs(metadata, fs, execute=None): - metadata_path = os.path.join(fs, "meta.js") metadata = dict([(m.key, m.value) for m in metadata]) - - utils.execute('tee', metadata_path, - process_input=jsonutils.dumps(metadata), run_as_root=True) + _inject_file_into_fs(fs, 'meta.js', jsonutils.dumps(metadata)) def _inject_key_into_fs(key, fs, execute=None): @@ -333,33 +352,36 @@ def _inject_key_into_fs(key, fs, execute=None): key is an ssh key string. fs is the path to the base of the filesystem into which to inject the key. """ - sshdir = os.path.join(fs, 'root', '.ssh') + sshdir = _join_and_check_path_within_fs(fs, 'root', '.ssh') utils.execute('mkdir', '-p', sshdir, run_as_root=True) utils.execute('chown', 'root', sshdir, run_as_root=True) utils.execute('chmod', '700', sshdir, run_as_root=True) - keyfile = os.path.join(sshdir, 'authorized_keys') - key_data = [ + + keyfile = os.path.join('root', '.ssh', 'authorized_keys') + + key_data = ''.join([ '\n', '# The following ssh key was injected by Nova', '\n', key.strip(), '\n', - ] - utils.execute('tee', '-a', keyfile, - process_input=''.join(key_data), run_as_root=True) + ]) + + _inject_file_into_fs(fs, keyfile, key_data, append=True) def _inject_net_into_fs(net, fs, execute=None): """Inject /etc/network/interfaces into the filesystem rooted at fs. net is the contents of /etc/network/interfaces. """ - netdir = os.path.join(os.path.join(fs, 'etc'), 'network') + netdir = _join_and_check_path_within_fs(fs, 'etc', 'network') utils.execute('mkdir', '-p', netdir, run_as_root=True) utils.execute('chown', 'root:root', netdir, run_as_root=True) utils.execute('chmod', 755, netdir, run_as_root=True) - netfile = os.path.join(netdir, 'interfaces') - utils.execute('tee', netfile, process_input=net, run_as_root=True) + + netfile = os.path.join('etc', 'network', 'interfaces') + _inject_file_into_fs(fs, netfile, net) def _inject_admin_password_into_fs(admin_passwd, fs, execute=None): @@ -384,16 +406,15 @@ def _inject_admin_password_into_fs(admin_passwd, fs, execute=None): fd, tmp_shadow = tempfile.mkstemp() os.close(fd) - 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) + 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) _set_passwd(admin_user, admin_passwd, tmp_passwd, tmp_shadow) - utils.execute('cp', tmp_passwd, os.path.join(fs, 'etc', 'passwd'), - run_as_root=True) + utils.execute('cp', tmp_passwd, passwd_path, run_as_root=True) os.unlink(tmp_passwd) - utils.execute('cp', tmp_shadow, os.path.join(fs, 'etc', 'shadow'), - run_as_root=True) + utils.execute('cp', tmp_shadow, shadow_path, run_as_root=True) os.unlink(tmp_shadow)
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
12- github.com/openstack/nova/commit/b0feaffdb2b1c51182b8dce41b367f3449af5dd9nvdExploitPatchWEB
- secunia.com/advisories/49763nvdVendor Advisory
- secunia.com/advisories/49802nvdVendor Advisory
- github.com/advisories/GHSA-m454-cm7h-rqhhghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2012-3360ghsaADVISORY
- www.ubuntu.com/usn/USN-1497-1nvdWEB
- bugs.launchpad.net/nova/+bug/1015531nvdWEB
- github.com/openstack/nova/commit/2427d4a99bed35baefd8f17ba422cb7aae8dcca7nvdWEB
- github.com/pypa/advisory-database/tree/main/vulns/nova/PYSEC-2012-38.yamlghsaWEB
- lists.launchpad.net/openstack/msg14089.htmlnvdWEB
- lists.fedoraproject.org/pipermail/package-announce/2012-July/083984.htmlnvd
- www.securityfocus.com/bid/54277nvd
News mentions
0No linked articles in our index yet.