From b3de0b9beea92183e86c07cd7e824999d616cb64 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Wed, 4 Mar 2026 10:49:09 -0500 Subject: [PATCH] Enforce IF-Match for DELETE requests as well --- netbox/netbox/api/viewsets/__init__.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/netbox/netbox/api/viewsets/__init__.py b/netbox/netbox/api/viewsets/__init__.py index f257e3966..95b2351d3 100644 --- a/netbox/netbox/api/viewsets/__init__.py +++ b/netbox/netbox/api/viewsets/__init__.py @@ -308,6 +308,9 @@ class NetBoxModelViewSet( def destroy(self, request, *args, **kwargs): instance = self.get_object_with_snapshot() + # Enforce If-Match precondition (RFC 9110 ยง13.1.1) + self._validate_etag(request, instance) + # Attach changelog message (if any) serializer = ChangeLogMessageSerializer(data=request.data) serializer.is_valid(raise_exception=True) @@ -322,7 +325,16 @@ class NetBoxModelViewSet( logger = logging.getLogger(f'netbox.api.views.{self.__class__.__name__}') logger.info(f"Deleting {model._meta.verbose_name} {instance} (PK: {instance.pk})") - return super().perform_destroy(instance) + try: + with transaction.atomic(using=router.db_for_write(model)): + # Re-check the If-Match ETag under a row-level lock to close the TOCTOU window + # between the initial check in destroy() and the actual delete. + if self._get_if_match(self.request): + locked = model.objects.select_for_update().get(pk=instance.pk) + self._validate_etag(self.request, locked) + super().perform_destroy(instance) + except ObjectDoesNotExist: + raise PermissionDenied() class MPTTLockedMixin: