Skip to content

Commit e75944f

Browse files
Maffoochclaude
andcommitted
Use a dedicated permission class for BurpRawRequestResponseViewSet
The top-level /api/v2/request_response_pairs/ viewset reused UserHasFindingRelatedObjectPermission, which is shaped for @action(detail=True) endpoints where DRF resolves the parent finding from the URL. On a top-level POST there is no parent object resolved yet, so the create flow only ran has_object_permission against the not-yet-saved row and effectively skipped any check on the client-supplied "finding" foreign key. Introduce UserHasBurpRawRequestResponsePermission, which validates the parent finding against Finding_Edit on POST via check_post_permission, mirroring the pattern already used by UserHasFindingPermission, UserHasProductPermission, and the other parent-keyed viewsets. has_object_permission dereferences obj.finding for retrieve/update/delete so list/detail/PUT/PATCH/DELETE behavior is unchanged. Add regression coverage in unittests/test_rest_framework.py asserting the positive control still works, that an authenticated user without membership cannot create a pair on a hidden finding, and that POSTs missing the finding key are rejected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b70c293 commit e75944f

3 files changed

Lines changed: 78 additions & 1 deletion

File tree

dojo/api_v2/permissions.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,23 @@ class UserHasFindingNotePermission(BaseRelatedObjectPermission):
455455
}
456456

457457

458+
class UserHasBurpRawRequestResponsePermission(permissions.BasePermission):
459+
def has_permission(self, request, view):
460+
return check_post_permission(
461+
request, Finding, "finding", Permissions.Finding_Edit,
462+
)
463+
464+
def has_object_permission(self, request, view, obj):
465+
return check_object_permission(
466+
request,
467+
obj.finding,
468+
Permissions.Finding_View,
469+
Permissions.Finding_Edit,
470+
Permissions.Finding_Edit,
471+
Permissions.Finding_Edit,
472+
)
473+
474+
458475
class UserHasImportPermission(permissions.BasePermission):
459476
def has_permission(self, request, view):
460477
# permission check takes place before validation, so we don't have access to serializer.validated_data()

dojo/api_v2/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3011,7 +3011,7 @@ class BurpRawRequestResponseViewSet(
30113011
filterset_fields = ["finding"]
30123012
permission_classes = (
30133013
IsAuthenticated,
3014-
permissions.UserHasFindingRelatedObjectPermission,
3014+
permissions.UserHasBurpRawRequestResponsePermission,
30153015
)
30163016

30173017
def get_queryset(self):

unittests/test_rest_framework.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1788,6 +1788,66 @@ def test_request_response_get(self):
17881788
self.assertEqual(200, response.status_code, response.content[:1000])
17891789

17901790

1791+
class RequestResponsePairsAuthzTest(DojoAPITestCase):
1792+
1793+
fixtures = ["dojo_testdata.json"]
1794+
1795+
def _client_for(self, username):
1796+
user = User.objects.get(username=username)
1797+
token = Token.objects.get(user=user)
1798+
client = APIClient()
1799+
client.credentials(HTTP_AUTHORIZATION="Token " + token.key)
1800+
return client
1801+
1802+
def test_admin_can_create_request_response_pair_positive_control(self):
1803+
client = self._client_for("admin")
1804+
before = BurpRawRequestResponse.objects.filter(finding_id=7).count()
1805+
response = client.post(
1806+
"/api/v2/request_response_pairs/",
1807+
dumps({
1808+
"finding": 7,
1809+
"burpRequestBase64": "cmVxdWVzdAo=",
1810+
"burpResponseBase64": "cmVzcG9uc2UK",
1811+
}),
1812+
content_type="application/json",
1813+
)
1814+
self.assertEqual(response.status_code, 201, response.content[:1000])
1815+
self.assertEqual(BurpRawRequestResponse.objects.filter(finding_id=7).count(), before + 1)
1816+
1817+
def test_unrelated_user_cannot_create_request_response_pair_on_hidden_finding(self):
1818+
client = self._client_for("user2")
1819+
# Sanity: the victim finding is genuinely hidden from this user.
1820+
get_response = client.get("/api/v2/findings/7/")
1821+
self.assertEqual(get_response.status_code, 404)
1822+
1823+
before = BurpRawRequestResponse.objects.filter(finding_id=7).count()
1824+
response = client.post(
1825+
"/api/v2/request_response_pairs/",
1826+
dumps({
1827+
"finding": 7,
1828+
"burpRequestBase64": "cmVxdWVzdAo=",
1829+
"burpResponseBase64": "cmVzcG9uc2UK",
1830+
}),
1831+
content_type="application/json",
1832+
)
1833+
self.assertIn(response.status_code, (403, 404), response.content[:1000])
1834+
self.assertEqual(BurpRawRequestResponse.objects.filter(finding_id=7).count(), before)
1835+
1836+
def test_post_without_finding_returns_4xx(self):
1837+
client = self._client_for("user2")
1838+
response = client.post(
1839+
"/api/v2/request_response_pairs/",
1840+
dumps({
1841+
"burpRequestBase64": "cmVxdWVzdAo=",
1842+
"burpResponseBase64": "cmVzcG9uc2UK",
1843+
}),
1844+
content_type="application/json",
1845+
)
1846+
# check_post_permission raises ParseError (400) when "finding" is omitted.
1847+
self.assertGreaterEqual(response.status_code, 400)
1848+
self.assertLess(response.status_code, 500)
1849+
1850+
17911851
@versioned_fixtures
17921852
class FilesTest(DojoAPITestCase):
17931853
fixtures = ["dojo_testdata.json"]

0 commit comments

Comments
 (0)