mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-10 20:26:48 +00:00
fix(computes): a compute's hostname and login cannot inject a connection URI or ssh option
A compute's hostname and login go unquoted into the libvirt connection URI (qemu+ssh://login@host/system, qemu+tcp://host/system, qemu+tls://...) and, for an SSH compute, into the ssh command line of the DRBD status view. They were barely validated: validate_hostname used re.match with a negated class, so it caught only a leading special character, and login was never checked. A hostname like "192.0.2.1/system?command=/path" injected a libvirt URI parameter (verified: connecting ran the named command through the ssh wrapper), and TLS/SSH parameters such as no_verify, pkipath, socket or netcat could be added the same way; a login like "-oProxyCommand=..." became an ssh option. The DRF ComputeSerializer applied neither check, and validate_name tested a string literal instead of its argument, so it never raised. validate_hostname now accepts only an IPv4 address or a host name with fullmatch (no scheme, port, path, query, whitespace or IPv6 literal; IPv6 is left out until the URI building brackets it). validate_login accepts an empty value (TCP falls back to the server's authentication, the socket transport ignores the login) or a plain name with no leading "-". Both sit on the Compute model fields, so the forms and the DRF serializer (which copies model validators onto its fields) reject the values; a migration records the metadata. validate_name checks its argument. drbd_status validates the login and hostname before building login@host and ends ssh option parsing with "--"; an existing or bypassed bad row is skipped rather than run. Tests: hostname/login validators accept real values and reject URI and option injection; the SSH form and the serializer (create, and an empty TCP login) enforce them; drbd_status skips an injecting target. Checked the real test computes still validate. The new tests fail on the previous code.
This commit is contained in:
parent
c7a7c843fb
commit
17016e90cd
7 changed files with 189 additions and 21 deletions
53
computes/test_validators.py
Normal file
53
computes/test_validators.py
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
"""A compute's hostname and login go unquoted into libvirt connection URIs
|
||||
and an ssh command line; they are validated so neither can inject a URI
|
||||
parameter (e.g. ?command=, ?no_verify=) or an ssh option."""
|
||||
|
||||
import unittest
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
|
||||
from computes.validators import validate_hostname, validate_login, validate_name
|
||||
|
||||
|
||||
class HostnameValidatorTestCase(unittest.TestCase):
|
||||
def test_accepts_ipv4_and_host_names(self):
|
||||
for value in ("192.0.2.10", "localhost", "host.example.com", "a-b.example.", "host1"):
|
||||
with self.subTest(value=value):
|
||||
validate_hostname(value)
|
||||
|
||||
def test_rejects_uri_and_option_injection(self):
|
||||
for value in (
|
||||
"-x",
|
||||
"a b;c",
|
||||
"host:2222",
|
||||
"192.0.2.1/system?command=/evil.sh&a=",
|
||||
"host/system?no_verify=1",
|
||||
"qemu+ssh://host/system",
|
||||
"2001:db8::1",
|
||||
"",
|
||||
"host name",
|
||||
):
|
||||
with self.subTest(value=value):
|
||||
with self.assertRaises(ValidationError):
|
||||
validate_hostname(value)
|
||||
|
||||
|
||||
class LoginValidatorTestCase(unittest.TestCase):
|
||||
def test_accepts_plain_logins_and_empty(self):
|
||||
# empty: TCP falls back to server auth, the socket transport ignores it
|
||||
for value in ("", "root", "libvirt.user", "user-1", "_svc"):
|
||||
with self.subTest(value=value):
|
||||
validate_login(value)
|
||||
|
||||
def test_rejects_option_and_delimiter_injection(self):
|
||||
for value in ("-oProxyCommand=x", "u@realm", "a/b", "a b", "a\\b", "a%b"):
|
||||
with self.subTest(value=value):
|
||||
with self.assertRaises(ValidationError):
|
||||
validate_login(value)
|
||||
|
||||
|
||||
class NameValidatorTestCase(unittest.TestCase):
|
||||
def test_checks_the_value(self):
|
||||
validate_name("good-name_1.2")
|
||||
with self.assertRaises(ValidationError):
|
||||
validate_name("bad name!")
|
||||
Loading…
Add table
Add a link
Reference in a new issue