1
0
Fork 0
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:
catborise 2026-10-06 22:10:19 +03:00
parent c7a7c843fb
commit 17016e90cd
7 changed files with 189 additions and 21 deletions

View file

@ -4,30 +4,42 @@ from ipaddress import ip_address
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
have_symbol = re.compile("[^a-zA-Z0-9._-]+")
wrong_ip = re.compile("^0.|^255.")
wrong_name = re.compile("[^a-zA-Z0-9._-]+")
# one or more dot-separated RFC 1123 labels, optional trailing dot
_hostname = re.compile(r"[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*\.?")
_login = re.compile(r"[A-Za-z0-9._][A-Za-z0-9._-]*")
_name = re.compile(r"[A-Za-z0-9._-]+")
def validate_hostname(value):
sym = have_symbol.match(value)
wip = wrong_ip.match(value)
"""An IPv4 address or a host name that goes safely into a libvirt
connection URI. No scheme, port, path, query, whitespace or IPv6 literal:
those would change the URI (an injected ?command=, ?no_verify=, a port or
a userinfo separator). IPv6 is rejected until the URI building brackets
it."""
try:
if ip_address(value).version == 4:
return
except ValueError:
if _hostname.fullmatch(value):
return
raise ValidationError(_("Enter an IPv4 address or a host name, without a scheme, port or path"))
if sym:
def validate_login(value):
"""A user name that goes safely into a libvirt connection URI and an ssh
command line. Empty is allowed (TCP falls back to the server's own
authentication, the socket transport ignores the login); otherwise only
letters, digits, ".", "_" and "-", and never a leading "-" (an ssh
option)."""
if value and not _login.fullmatch(value):
raise ValidationError(
_('Hostname must contain only numbers, or the domain name separated by "."')
_("The login may contain only letters, digits, '.', '_' and '-', and may not start with '-'")
)
elif wip:
raise ValidationError(_("Wrong IP address"))
def validate_name(value):
have_symbol = wrong_name.match("[^a-zA-Z0-9._-]+")
if have_symbol:
raise ValidationError(_("The hostname must not contain any special characters"))
_hostname = re.compile(r"[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*\.?")
if not _name.fullmatch(value):
raise ValidationError(_("The name must not contain any special characters"))
def validate_migration_address(value):