mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-09 11:46:49 +00:00
create_storage_ceph and create_storage_netfs wrote the pool, host, user, secret and format values into the pool XML unescaped and unchecked: the page's patterns were the only guard, and a direct POST could break or extend the XML. The values now go through util.xml_escape, as the network code does. The page's host fields stopped at 20 characters, so nfs01.storage.example.com could not be typed: they take a host name's 253; the Ceph pool and user fields lose the limit the server never had.
79 lines
3.4 KiB
Python
79 lines
3.4 KiB
Python
"""The storage pool target and source checks run in linear time and take
|
|
paths of letters, digits and "/", "_", ".", ":" and "-" (not first, not right
|
|
after "/"): both values go into the pool XML."""
|
|
|
|
import time
|
|
from types import SimpleNamespace
|
|
|
|
from django.template.loader import render_to_string
|
|
from django.test import SimpleTestCase
|
|
from lxml import html
|
|
from storages.forms import AddStgPool
|
|
|
|
|
|
def target_error(target):
|
|
form = AddStgPool({"name": "pool1", "stg_type": "dir", "target": target})
|
|
form.is_valid()
|
|
return "target" in form.errors
|
|
|
|
|
|
def source_error(source, stg_type="logical"):
|
|
form = AddStgPool({"name": "pool1", "stg_type": stg_type, "source": source, "target": "/mnt/pool1"})
|
|
form.is_valid()
|
|
return "source" in form.errors
|
|
|
|
|
|
class PoolTargetTests(SimpleTestCase):
|
|
def test_accepted_and_refused_targets(self):
|
|
for target, refused in (
|
|
("/var/lib/libvirt/images", False),
|
|
("/srv/iso-pool", False),
|
|
("/a//b", False),
|
|
("a-b.c_d/e", False),
|
|
("/a/-b", True), # a dash right after a slash
|
|
("-x", True),
|
|
("/a b", True),
|
|
("'/a", True), # any first character but "-" passed
|
|
("</a", True),
|
|
):
|
|
with self.subTest(target=target):
|
|
self.assertEqual(target_error(target), refused)
|
|
|
|
def test_accepted_and_refused_sources(self):
|
|
for source, refused in (
|
|
("/dev/vdb", False),
|
|
("/dev/mapper/vg0-data", False),
|
|
("/dev/disk/by-id/wwn-0x5000c500a1b2c3d4", False),
|
|
("/dev/disk/by-path/pci-0000:00:1f.2-ata-1", False),
|
|
("/export/vm_images.v2", False),
|
|
("", True),
|
|
("-x", True), # would read as an option
|
|
("/a/-b", True),
|
|
("/a b", True),
|
|
("/a'b", True), # the value goes into the pool XML
|
|
("/a<b", True),
|
|
("'/a", True),
|
|
):
|
|
for stg_type in ("logical", "netfs"):
|
|
with self.subTest(source=source, stg_type=stg_type):
|
|
self.assertEqual(source_error(source, stg_type), refused)
|
|
|
|
def test_a_crafted_target_is_checked_at_once(self):
|
|
# the previous expression backtracked exponentially: 22 dashes took over a second
|
|
start = time.monotonic()
|
|
self.assertTrue(target_error("," + "-" * 98 + "!"))
|
|
self.assertLess(time.monotonic() - start, 1)
|
|
|
|
def test_the_create_form_leaves_the_path_check_to_the_server(self):
|
|
# a stricter pattern or a shorter maxlength in the page refuses paths the form accepts
|
|
superuser = SimpleNamespace(user=SimpleNamespace(is_superuser=True))
|
|
page = html.fromstring(render_to_string("create_stg_block.html", {"request": superuser}))
|
|
fields = page.xpath("//input[@name='target' or @name='source']")
|
|
self.assertEqual(len(fields), 5)
|
|
for field in fields:
|
|
with self.subTest(form=field.xpath("string(ancestor::form//input[@name='stg_type']/@value)"), name=field.name):
|
|
self.assertIsNone(field.get("pattern"))
|
|
self.assertGreaterEqual(int(field.get("maxlength", 100)), 100)
|
|
for name in ("netfs_host", "ceph_host"): # a host name is up to 253 characters
|
|
with self.subTest(name=name):
|
|
self.assertGreaterEqual(int(page.xpath(f"//input[@name='{name}']/@maxlength")[0]), 253)
|