1
0
Fork 0
mirror of https://github.com/retspen/webvirtcloud synced 2026-11-08 19:26:48 +00:00

fix(interfaces): validate IPv6 address and gateway of host interfaces

The form checked both with re.match("^[0-9a-f./:]+|^$"), which tests
only how a value starts: "2001:db8::10/64'/><evil/>" passed and went
into the interface XML, which is built with f-strings, while upper-case
hex was refused. An address without a prefix passed too and then failed
with a ValueError when the XML was built.

The address is parsed with ipaddress.IPv6Interface and stored as
address/prefix (/128 when none is given), the gateway with
IPv6Address. A static configuration needs an address, and the XML gets
a route only when a gateway is given instead of gateway=''. The HTML
patterns accept upper-case hex.

Eight of the ten form and XML checks fail on the previous code.
This commit is contained in:
catborise 2026-10-04 17:29:42 +03:00
parent 78ab85f211
commit 525018dd99
4 changed files with 95 additions and 25 deletions

View file

@ -1,3 +1,4 @@
import ipaddress
import re
from django import forms
@ -57,29 +58,28 @@ class AddInterface(forms.Form):
def clean_ipv6_addr(self):
ipv6_addr = self.cleaned_data["ipv6_addr"]
have_symbol = re.match("^[0-9a-f./:]+|^$", ipv6_addr)
if not have_symbol:
raise forms.ValidationError(
_("The IPv6 address must not contain any special characters")
)
elif len(ipv6_addr) > 100:
raise forms.ValidationError(
_("The IPv6 address must not exceed 100 characters")
)
return ipv6_addr
if not ipv6_addr:
return ipv6_addr
try:
# Always address/prefix, as the interface XML needs both
return str(ipaddress.IPv6Interface(ipv6_addr))
except ValueError:
raise forms.ValidationError(_("The IPv6 address is not valid (e.g. 2001:db8::10/64)"))
def clean_ipv6_gw(self):
ipv6_gw = self.cleaned_data["ipv6_gw"]
have_symbol = re.match("^[0-9a-f./:]+|^$", ipv6_gw)
if not have_symbol:
raise forms.ValidationError(
_("The IPv6 gateway must not contain any special characters")
)
elif len(ipv6_gw) > 100:
raise forms.ValidationError(
_("The IPv6 gateway must not exceed 100 characters")
)
return ipv6_gw
if not ipv6_gw:
return ipv6_gw
try:
return str(ipaddress.IPv6Address(ipv6_gw))
except ValueError:
raise forms.ValidationError(_("The IPv6 gateway is not valid"))
def clean(self):
cleaned = super().clean()
if cleaned.get("ipv6_type") == "static" and not cleaned.get("ipv6_addr") and "ipv6_addr" not in self.errors:
self.add_error("ipv6_addr", _("A static IPv6 configuration needs an address"))
return cleaned
def clean_name(self):
name = self.cleaned_data["name"]

View file

@ -122,13 +122,13 @@
<div class="row static_ipv6_form_group">
<label class="col-sm-3 col-form-label">{% trans "IPv6 Address" %}</label>
<div class="col-sm-6">
<input type="text" class="form-control" name="ipv6_addr" maxlength="100" pattern="[0-9a-f\:\/]+|^$">
<input type="text" class="form-control" name="ipv6_addr" maxlength="100" pattern="[0-9a-fA-F\:\/]+|^$">
</div>
</div>
<div class="row static_ipv6_form_group">
<label class="col-sm-3 col-form-label">{% trans "IPv6 Gateway" %}</label>
<div class="col-sm-6">
<input type="text" class="form-control" name="ipv6_gw" maxlength="100" pattern="[0-9a-f\:]+|^$">
<input type="text" class="form-control" name="ipv6_gw" maxlength="100" pattern="[0-9a-fA-F\:]+|^$">
</div>
</div>
</div>

69
interfaces/test_forms.py Normal file
View file

@ -0,0 +1,69 @@
"""Host interface form: IPv6 values are validated before they go into the interface XML."""
from unittest.mock import MagicMock
from django.test import SimpleTestCase
from lxml import etree
from vrtManager.interface import wvmInterfaces
from interfaces.forms import AddInterface
BASE = {
"name": "br1", "itype": "bridge", "start_mode": "onboot", "netdev": "eth1",
"ipv4_type": "static", "ipv4_addr": "192.0.2.10/24", "ipv4_gw": "192.0.2.1",
"ipv6_type": "static", "stp": "on", "delay": 0,
}
def form(**ipv6):
return AddInterface(data={**BASE, **ipv6})
class Ipv6FormTestCase(SimpleTestCase):
def test_valid_addresses_are_normalized(self):
f = form(ipv6_addr="2001:DB8::10/64", ipv6_gw="2001:DB8::1")
self.assertTrue(f.is_valid(), f.errors)
self.assertEqual(f.cleaned_data["ipv6_addr"], "2001:db8::10/64")
self.assertEqual(f.cleaned_data["ipv6_gw"], "2001:db8::1")
def test_address_without_prefix_gets_one(self):
f = form(ipv6_addr="2001:db8::10", ipv6_gw="")
self.assertTrue(f.is_valid(), f.errors)
self.assertEqual(f.cleaned_data["ipv6_addr"], "2001:db8::10/128")
def test_values_that_only_start_like_an_address_are_rejected(self):
for field, value in (
("ipv6_addr", "2001:db8::10/64'/><evil/>"),
("ipv6_gw", "2001:db8::1'/><evil/>"),
("ipv6_addr", "2001:db8::zz/64"),
("ipv6_addr", "192.0.2.10/24"),
):
with self.subTest(field=field, value=value):
f = form(**{"ipv6_addr": "2001:db8::10/64", "ipv6_gw": "", field: value})
self.assertFalse(f.is_valid())
self.assertIn(field, f.errors)
def test_static_needs_an_address(self):
f = form(ipv6_addr="", ipv6_gw="")
self.assertFalse(f.is_valid())
self.assertIn("ipv6_addr", f.errors)
def test_dhcp_needs_no_address(self):
self.assertTrue(form(ipv6_type="dhcp", ipv6_addr="", ipv6_gw="").is_valid())
class Ipv6XmlTestCase(SimpleTestCase):
def xml(self, gw):
conn = wvmInterfaces.__new__(wvmInterfaces)
conn.define_iface = MagicMock()
conn.get_iface = MagicMock()
conn.create_iface("br1", "bridge", "onboot", "eth1", "none", "", "", "static", "2001:db8::10/64", gw, "on", 0)
return etree.fromstring(conn.define_iface.call_args[0][0])
def test_static_address_with_gateway(self):
proto = self.xml("2001:db8::1").find("protocol[@family='ipv6']")
self.assertEqual(proto.find("ip").attrib, {"address": "2001:db8::10", "prefix": "64"})
self.assertEqual(proto.find("route").get("gateway"), "2001:db8::1")
def test_no_empty_route_without_gateway(self):
self.assertIsNone(self.xml("").find("protocol[@family='ipv6']/route"))

View file

@ -44,9 +44,10 @@ class wvmInterfaces(wvmConnect):
if ipv6_type == "static":
address, prefix = ipv6_addr.split("/")
xml += f"""<protocol family='ipv6'>
<ip address='{address}' prefix='{prefix}'/>
<route gateway='{ipv6_gw}'/>
</protocol>"""
<ip address='{address}' prefix='{prefix}'/>"""
if ipv6_gw:
xml += f"""<route gateway='{ipv6_gw}'/>"""
xml += """</protocol>"""
if itype == "bridge":
xml += f"""<bridge stp='{stp}' delay='{delay}'>
<interface name='{netdev}' type='ethernet'/>