1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-11 12:46:49 +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

@ -0,0 +1,17 @@
"""drbd_status runs ssh with the compute's login@hostname; a compute whose
values would inject an ssh option (an existing or bypassed row) is skipped."""
import unittest
from types import SimpleNamespace
from instances.views import _valid_ssh_target
class DrbdTargetTestCase(unittest.TestCase):
def test_valid_target(self):
self.assertTrue(_valid_ssh_target(SimpleNamespace(login="root", hostname="192.0.2.1")))
def test_option_injection_is_skipped(self):
for login, hostname in (("-oProxyCommand=x", "192.0.2.1"), ("root", "-x"), ("root", "a b")):
with self.subTest(login=login, hostname=hostname):
self.assertFalse(_valid_ssh_target(SimpleNamespace(login=login, hostname=hostname)))

View file

@ -16,12 +16,13 @@ from admin.decorators import superuser_only
from appsettings.models import AppSettings
from appsettings.settings import app_settings
from computes.models import Compute
from computes.validators import validate_hostname, validate_login
from computes.utils import libvirt_compute_lock, libvirt_instance_lock, user_quota_lock
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.core.exceptions import PermissionDenied, ValidationError
from django.http import Http404, HttpResponse, JsonResponse
from django.db.models import prefetch_related_objects
from django.shortcuts import get_object_or_404, redirect, render
@ -194,14 +195,26 @@ def status(request, pk):
return JsonResponse({"status": instance.proxy.get_status()})
def _valid_ssh_target(compute):
"""The login and hostname reach ssh as a command-line argument; a compute
whose values would inject an ssh option (an existing or bypassed row that
the field validators did not cover) is skipped rather than run."""
try:
validate_login(compute.login)
validate_hostname(compute.hostname)
except ValidationError:
return False
return True
def drbd_status(request, pk):
instance = get_instance(request.user, pk)
result = "None DRBD"
if instance.compute.type == 2:
if instance.compute.type == 2 and _valid_ssh_target(instance.compute):
conn = instance.compute.login + "@" + instance.compute.hostname
remoteDrbdStatus = subprocess.run(
["ssh", conn, "sudo", "/usr/sbin/drbdadm", "status", "&&", "exit"],
["ssh", "--", conn, "sudo", "/usr/sbin/drbdadm", "status"],
stdout=subprocess.PIPE,
text=True,
)