mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
fix(instances): restrict disk views to superusers and validate disk input
Disk and media mutation views (add_new_vol, add_existing_vol, edit_volume, delete_vol, detach_vol, add_cdrom, detach_cdrom, mount_iso, unmount_iso) were reachable by any owner with is_change, although the UI only shows the Disk tab to superusers. An ordinary user could delete any volume on the compute or attach arbitrary host files to their VM. - Require superuser for all disk/media views, matching the UI. - delete_vol resolves the volume from the VM's own disk XML by dev and ignores the posted storage/name. - Validate bus/cache/io/discard/detect_zeroes against the hypervisor's lists, and format/serial/volume name against strict patterns. - add_existing_vol only accepts volumes that exist in the selected pool. - edit_volume only accepts a dev attached to the VM. - vrtManager: escape all values interpolated into disk XML and pass dev as an XPath variable in detach_disk/edit_disk.
This commit is contained in:
parent
b1ab1d47fa
commit
8a3ee13b41
5 changed files with 573 additions and 179 deletions
|
|
@ -760,6 +760,15 @@ class wvmInstance(wvmConnect):
|
|||
discard_mode=None,
|
||||
detect_zeroes_mode=None,
|
||||
):
|
||||
# Every value below is interpolated into disk XML; escape them all so
|
||||
# a quote in a path, serial or option cannot inject XML.
|
||||
esc = util.xml_escape
|
||||
target_dev, source, target_bus = esc(target_dev), esc(source), esc(target_bus)
|
||||
disk_type, disk_device = esc(disk_type), esc(disk_device)
|
||||
driver_name, format_type = esc(driver_name), esc(format_type)
|
||||
serial = esc(serial)
|
||||
cache_mode, io_mode = esc(cache_mode), esc(io_mode)
|
||||
discard_mode, detect_zeroes_mode = esc(discard_mode), esc(detect_zeroes_mode)
|
||||
|
||||
additionals = ""
|
||||
if (
|
||||
|
|
@ -822,7 +831,7 @@ class wvmInstance(wvmConnect):
|
|||
def detach_disk(self, target_dev):
|
||||
tree = etree.fromstring(self._XMLDesc(0))
|
||||
|
||||
disk_el = tree.xpath("./devices/disk/target[@dev='{}']".format(target_dev))[
|
||||
disk_el = tree.xpath("./devices/disk/target[@dev=$dev]", dev=target_dev)[
|
||||
0
|
||||
].getparent()
|
||||
xml_disk = etree.tostring(disk_el).decode()
|
||||
|
|
@ -850,7 +859,7 @@ class wvmInstance(wvmConnect):
|
|||
detect_zeroes_mode,
|
||||
):
|
||||
tree = etree.fromstring(self._XMLDesc(0))
|
||||
disk_el = tree.xpath("./devices/disk/target[@dev='{}']".format(target_dev))[
|
||||
disk_el = tree.xpath("./devices/disk/target[@dev=$dev]", dev=target_dev)[
|
||||
0
|
||||
].getparent()
|
||||
old_disk_type = disk_el.get("type")
|
||||
|
|
@ -858,6 +867,15 @@ class wvmInstance(wvmConnect):
|
|||
old_driver_name = disk_el.xpath("driver/@name")[0]
|
||||
old_target_bus = disk_el.xpath("target/@bus")[0]
|
||||
|
||||
# Values below are interpolated into disk XML; escape them all.
|
||||
esc = util.xml_escape
|
||||
target_dev, source, target_bus = esc(target_dev), esc(source), esc(target_bus)
|
||||
serial, format = esc(serial), esc(format)
|
||||
cache_mode, io_mode = esc(cache_mode), esc(io_mode)
|
||||
discard_mode, detect_zeroes_mode = esc(discard_mode), esc(detect_zeroes_mode)
|
||||
old_disk_type, old_disk_device = esc(old_disk_type), esc(old_disk_device)
|
||||
old_driver_name = esc(old_driver_name)
|
||||
|
||||
additionals = ""
|
||||
if cache_mode is not None and cache_mode != "default":
|
||||
additionals += f"cache='{cache_mode}' "
|
||||
|
|
|
|||
|
|
@ -178,3 +178,67 @@ class TestXmlSafety(unittest.TestCase):
|
|||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
|
||||
class TestDiskXmlEscaping(unittest.TestCase):
|
||||
def _instance(self):
|
||||
inst = wvmInstance.__new__(wvmInstance)
|
||||
inst.get_status = MagicMock(return_value=5) # shutoff
|
||||
inst.instance = MagicMock()
|
||||
return inst
|
||||
|
||||
def test_attach_disk_escapes_source_and_serial(self):
|
||||
inst = self._instance()
|
||||
inst.attach_disk(
|
||||
"vdb",
|
||||
"/images/a'/><disk type='file",
|
||||
target_bus="virtio",
|
||||
disk_type="file",
|
||||
serial="S1</serial><evil/>",
|
||||
)
|
||||
xml = inst.instance.attachDeviceFlags.call_args[0][0]
|
||||
self.assertIn("file='/images/a'/><disk type='file'", xml)
|
||||
self.assertIn("<serial>S1</serial><evil/></serial>", xml)
|
||||
self.assertNotIn("<evil/>", xml)
|
||||
|
||||
def test_edit_disk_escapes_source_and_serial(self):
|
||||
inst = self._instance()
|
||||
inst._XMLDesc = MagicMock(
|
||||
return_value=(
|
||||
"<domain><devices><disk type='file' device='disk'>"
|
||||
"<driver name='qemu' type='qcow2'/>"
|
||||
"<source file='/images/vm.qcow2'/>"
|
||||
"<target dev='vda' bus='virtio'/>"
|
||||
"</disk></devices></domain>"
|
||||
)
|
||||
)
|
||||
inst.edit_disk(
|
||||
"vda",
|
||||
"/images/b'/><x y='",
|
||||
False,
|
||||
False,
|
||||
"virtio",
|
||||
"S2<evil/>",
|
||||
"qcow2",
|
||||
"default",
|
||||
"default",
|
||||
"default",
|
||||
"default",
|
||||
)
|
||||
xml = inst.instance.updateDeviceFlags.call_args[0][0]
|
||||
self.assertIn("file='/images/b'/><x y=''", xml)
|
||||
self.assertNotIn("<evil/>", xml)
|
||||
|
||||
def test_detach_disk_does_not_evaluate_dev_as_xpath(self):
|
||||
inst = self._instance()
|
||||
inst._XMLDesc = MagicMock(
|
||||
return_value=(
|
||||
"<domain><devices><disk type='file' device='disk'>"
|
||||
"<source file='/images/vm.qcow2'/>"
|
||||
"<target dev='vda' bus='virtio'/>"
|
||||
"</disk></devices></domain>"
|
||||
)
|
||||
)
|
||||
with self.assertRaises(IndexError):
|
||||
inst.detach_disk("x' or '1'='1")
|
||||
inst.instance.detachDeviceFlags.assert_not_called()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue