From 6d51b34c7e64b719b2122a4fc7cfd24d01941610 Mon Sep 17 00:00:00 2001 From: Federico Capoano Date: Mon, 15 Jun 2026 16:08:01 -0300 Subject: [PATCH] [fix] Fixed admin subnet export multitenancy security issue Before this patch, if a specific subnet ID was known, any staff user with permissions to operate on subnet objects was allowed to export the subnet contents, regardless of whether they managed the organization of the subnet or not. This patch fixes it. (cherry picked from commit a4b272461bfa7a1762baf0b1fd76b4f5b681586b) --- openwisp_ipam/admin.py | 6 ++++-- openwisp_ipam/tests/test_multitenant.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/openwisp_ipam/admin.py b/openwisp_ipam/admin.py index 2a7b904..3720a73 100644 --- a/openwisp_ipam/admin.py +++ b/openwisp_ipam/admin.py @@ -10,7 +10,7 @@ from django.db.models import TextField from django.db.models.functions import Cast from django.http import HttpResponse -from django.shortcuts import redirect, render +from django.shortcuts import get_object_or_404, redirect, render from django.urls import path, re_path, reverse from django.utils.translation import gettext_lazy as _ from openwisp_users.multitenancy import MultitenantAdminMixin, MultitenantOrgFilter @@ -145,10 +145,12 @@ def get_urls(self): return custom_urls + urls def export_view(self, request, subnet_id): + # Returns 404 if user is not a manager of the subnet's organization + subnet = get_object_or_404(self.get_queryset(request), pk=subnet_id) response = HttpResponse(content_type="text/csv") response["Content-Disposition"] = 'attachment; filename="ip_address.csv"' writer = csv.writer(response) - Subnet().export_csv(subnet_id, writer) + Subnet().export_csv(subnet.id, writer) return response def import_view(self, request): diff --git a/openwisp_ipam/tests/test_multitenant.py b/openwisp_ipam/tests/test_multitenant.py index bfd8ba9..1c50634 100644 --- a/openwisp_ipam/tests/test_multitenant.py +++ b/openwisp_ipam/tests/test_multitenant.py @@ -96,6 +96,19 @@ def test_import_subnet_permission(self): self.assertContains(response, '
  • You do not have') self.assertEqual(Subnet.objects.count(), 3) + def test_export_subnet_multitenancy(self): + data = self._create_multitenancy_test_env() + self._create_administrator( + organizations=[data["org1"]], + username="org1_administrator", + email="org1administrator@test.com", + ) + self._login(username="org1_administrator", password="tester") + response = self.client.get( + reverse("admin:ipam_export_subnet", args=(data["subnet2"].id,)) + ) + self.assertEqual(response.status_code, 404) + class TestMultitenantApi( TestMultitenantAdminMixin, CreateModelsMixin, PostDataMixin, TestCase