Skip to content

Commit f2a8d3d

Browse files
mruegclaude
andauthored
Validate VLAN ID path parameter (#7329)
* Validate VLAN ID path parameter Creating a VLAN with a non-numeric ID (e.g. /vlan/abc) raised a ValueError from int() and surfaced as a 500 error. VLAN IDs outside the valid 802.1Q range were also passed on to NetworkManager unchecked. Validate the VLAN ID as an integer in 1..4094, so clients get a 400 with a clear message. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> * Allow VLAN ID 0 for 802.1p priority tagging Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 02dfc50 commit f2a8d3d

2 files changed

Lines changed: 48 additions & 6 deletions

File tree

‎supervisor/api/network.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,9 @@ def _require_psk_for_wpa_psk(
249249
}
250250
)
251251

252+
# VLAN ID 0 is deliberately allowed for 802.1p priority tagging, 4095 is reserved
253+
VALIDATE_VLAN_ID = vol.All(vol.Coerce(int), vol.Range(min=0, max=4094))
254+
252255

253256
def ip4config_struct(config: IpConfig, setting: IpSetting) -> dict[str, Any]:
254257
"""Return a dict with information about IPv4 configuration."""
@@ -692,9 +695,13 @@ async def scan_accesspoints(self, request: web.Request) -> dict[str, Any]:
692695
async def create_vlan(self, request: web.Request) -> None:
693696
"""Create a new vlan."""
694697
interface = self._get_interface(request.match_info[ATTR_INTERFACE])
695-
vlan = int(request.match_info.get(ATTR_VLAN, -1))
696-
if vlan < 0:
697-
raise APIError(f"Invalid vlan specified: {vlan}")
698+
vlan_id = request.match_info[ATTR_VLAN]
699+
try:
700+
vlan: int = VALIDATE_VLAN_ID(vlan_id)
701+
except vol.Invalid as err:
702+
raise APIError(
703+
f"Invalid VLAN ID {vlan_id}, must be between 0 and 4094"
704+
) from err
698705

699706
# Only ethernet is supported
700707
if interface.type != InterfaceType.ETHERNET:

‎tests/api/test_network.py‎

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -412,16 +412,24 @@ async def test_api_network_reload(
412412
assert network_manager_service.CheckConnectivity.calls == [()]
413413

414414

415+
@pytest.mark.parametrize(
416+
"vlan",
417+
[
418+
pytest.param(1, id="vlan"),
419+
pytest.param(0, id="priority-tagging"),
420+
],
421+
)
415422
async def test_api_network_vlan(
416423
api_client: TestClient,
417424
coresys: CoreSys,
418425
network_manager_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
426+
vlan: int,
419427
):
420428
"""Test creating a vlan."""
421429
settings_service: SettingsService = network_manager_services["network_settings"]
422430
settings_service.AddConnection.calls.clear()
423431
resp = await api_client.post(
424-
f"/network/interface/{TEST_INTERFACE_ETH_NAME}/vlan/1",
432+
f"/network/interface/{TEST_INTERFACE_ETH_NAME}/vlan/{vlan}",
425433
json={"ipv4": {"method": "auto"}, "llmnr": "off"},
426434
)
427435
result = await resp.json()
@@ -431,7 +439,7 @@ async def test_api_network_vlan(
431439
connection = settings_service.AddConnection.calls[0][0]
432440
assert "uuid" in connection["connection"]
433441
assert connection["connection"] == {
434-
"id": Variant("s", "Supervisor eth0.1"),
442+
"id": Variant("s", f"Supervisor eth0.{vlan}"),
435443
"type": Variant("s", "vlan"),
436444
"mdns": Variant("i", -1), # Default mode
437445
"llmnr": Variant("i", 0),
@@ -441,7 +449,7 @@ async def test_api_network_vlan(
441449
assert connection["ipv4"] == {"method": Variant("s", "auto")}
442450
assert connection["ipv6"] == {"method": Variant("s", "auto")}
443451
assert connection["vlan"] == {
444-
"id": Variant("u", 1),
452+
"id": Variant("u", vlan),
445453
"flags": Variant("u", 1),
446454
"parent": Variant("s", "0c23631e-2118-355c-bbb0-8943229cb0d6"),
447455
}
@@ -457,6 +465,33 @@ async def test_api_network_vlan(
457465
assert len(settings_service.AddConnection.calls) == 1
458466

459467

468+
@pytest.mark.parametrize(
469+
"vlan",
470+
[
471+
pytest.param("abc", id="not-a-number"),
472+
pytest.param("1.5", id="float"),
473+
pytest.param("4095", id="reserved-4095"),
474+
pytest.param("-1", id="negative"),
475+
],
476+
)
477+
async def test_api_network_vlan_invalid_id(
478+
api_client: TestClient,
479+
network_manager_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
480+
vlan: str,
481+
):
482+
"""Test creating a vlan with an invalid id returns a client error."""
483+
settings_service: SettingsService = network_manager_services["network_settings"]
484+
settings_service.AddConnection.calls.clear()
485+
resp = await api_client.post(
486+
f"/network/interface/{TEST_INTERFACE_ETH_NAME}/vlan/{vlan}",
487+
json={"ipv4": {"method": "auto"}},
488+
)
489+
assert resp.status == 400
490+
body = await resp.json()
491+
assert body["message"] == f"Invalid VLAN ID {vlan}, must be between 0 and 4094"
492+
assert settings_service.AddConnection.calls == []
493+
494+
460495
@pytest.mark.parametrize(
461496
("method", "url"),
462497
[

0 commit comments

Comments
 (0)