Skip to content

Commit daaaa71

Browse files
devGregAclaude
andcommitted
Align get_object_or_404 filtering strategy with dev branch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5ee3dc4 commit daaaa71

6 files changed

Lines changed: 20 additions & 110 deletions

File tree

dojo/engagement/views.py

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1377,10 +1377,7 @@ def edit_risk_acceptance(request, eid, raid):
13771377
# will only be called by view_risk_acceptance and edit_risk_acceptance
13781378
def view_edit_risk_acceptance(request, eid, raid, *, edit_mode=False):
13791379
risk_acceptance = get_object_or_404(Risk_Acceptance, pk=raid)
1380-
eng = get_object_or_404(Engagement, pk=eid)
1381-
# Ensure the risk acceptance belongs to the supplied engagement
1382-
if not Engagement.objects.filter(risk_acceptance=risk_acceptance, id=eid).exists():
1383-
raise PermissionDenied
1380+
eng = get_object_or_404(Engagement.objects.filter(risk_acceptance=risk_acceptance), pk=eid)
13841381

13851382
if edit_mode and not eng.product.enable_full_risk_acceptance:
13861383
raise PermissionDenied
@@ -1540,11 +1537,7 @@ def view_edit_risk_acceptance(request, eid, raid, *, edit_mode=False):
15401537
@user_is_authorized(Engagement, Permissions.Risk_Acceptance, "eid")
15411538
def expire_risk_acceptance(request, eid, raid):
15421539
risk_acceptance = get_object_or_404(prefetch_for_expiration(Risk_Acceptance.objects.all()), pk=raid)
1543-
# Validate the engagement ID exists before moving forward
1544-
get_object_or_404(Engagement, pk=eid)
1545-
# Ensure the risk acceptance belongs to the supplied engagement
1546-
if not Engagement.objects.filter(risk_acceptance=risk_acceptance, id=eid).exists():
1547-
raise PermissionDenied
1540+
get_object_or_404(Engagement.objects.filter(risk_acceptance=risk_acceptance), pk=eid)
15481541

15491542
ra_helper.expire_now(risk_acceptance)
15501543

@@ -1554,10 +1547,7 @@ def expire_risk_acceptance(request, eid, raid):
15541547
@user_is_authorized(Engagement, Permissions.Risk_Acceptance, "eid")
15551548
def reinstate_risk_acceptance(request, eid, raid):
15561549
risk_acceptance = get_object_or_404(prefetch_for_expiration(Risk_Acceptance.objects.all()), pk=raid)
1557-
eng = get_object_or_404(Engagement, pk=eid)
1558-
# Ensure the risk acceptance belongs to the supplied engagement
1559-
if not Engagement.objects.filter(risk_acceptance=risk_acceptance, id=eid).exists():
1560-
raise PermissionDenied
1550+
eng = get_object_or_404(Engagement.objects.filter(risk_acceptance=risk_acceptance), pk=eid)
15611551

15621552
if not eng.product.enable_full_risk_acceptance:
15631553
raise PermissionDenied
@@ -1570,10 +1560,7 @@ def reinstate_risk_acceptance(request, eid, raid):
15701560
@user_is_authorized(Engagement, Permissions.Risk_Acceptance, "eid")
15711561
def delete_risk_acceptance(request, eid, raid):
15721562
risk_acceptance = get_object_or_404(Risk_Acceptance, pk=raid)
1573-
eng = get_object_or_404(Engagement, pk=eid)
1574-
# Ensure the risk acceptance belongs to the supplied engagement
1575-
if not Engagement.objects.filter(risk_acceptance=risk_acceptance, id=eid).exists():
1576-
raise PermissionDenied
1563+
eng = get_object_or_404(Engagement.objects.filter(risk_acceptance=risk_acceptance), pk=eid)
15771564

15781565
ra_helper.delete(eng, risk_acceptance)
15791566

dojo/product/views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1597,7 +1597,7 @@ def engagement_presets(request, pid):
15971597
@user_is_authorized(Product, Permissions.Product_Edit, "pid")
15981598
def edit_engagement_presets(request, pid, eid):
15991599
prod = get_object_or_404(Product, id=pid)
1600-
preset = get_object_or_404(Engagement_Presets, id=eid, product_id=pid)
1600+
preset = get_object_or_404(Engagement_Presets.objects.filter(product=prod), id=eid)
16011601

16021602
product_tab = Product_Tab(prod, title=_("Edit Engagement Preset"), tab="settings")
16031603

@@ -1646,7 +1646,7 @@ def add_engagement_presets(request, pid):
16461646
@user_is_authorized(Product, Permissions.Product_Edit, "pid")
16471647
def delete_engagement_presets(request, pid, eid):
16481648
prod = get_object_or_404(Product, id=pid)
1649-
preset = get_object_or_404(Engagement_Presets, id=eid, product_id=pid)
1649+
preset = get_object_or_404(Engagement_Presets.objects.filter(product=prod), id=eid)
16501650
form = DeleteEngagementPresetsForm(instance=preset)
16511651

16521652
if request.method == "POST":

dojo/survey/views.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
@user_is_authorized(Engagement, Permissions.Engagement_Edit, "eid")
5858
def delete_engagement_survey(request, eid, sid):
5959
engagement = get_object_or_404(Engagement, id=eid)
60-
survey = get_object_or_404(Answered_Survey, id=sid, engagement_id=eid)
60+
survey = get_object_or_404(Answered_Survey.objects.filter(engagement=engagement), id=sid)
6161
questions = get_answered_questions(survey=survey, read_only=True)
6262
form = Delete_Questionnaire_Form(instance=survey)
6363

@@ -96,8 +96,8 @@ def delete_engagement_survey(request, eid, sid):
9696

9797

9898
def answer_questionnaire(request, eid, sid):
99-
survey = get_object_or_404(Answered_Survey, id=sid, engagement_id=eid)
10099
engagement = get_object_or_404(Engagement, id=eid)
100+
survey = get_object_or_404(Answered_Survey.objects.filter(engagement=engagement), id=sid)
101101
system_settings = System_Settings.objects.all()[0]
102102

103103
if not system_settings.allow_anonymous_survey_repsonse:
@@ -162,8 +162,8 @@ def answer_questionnaire(request, eid, sid):
162162

163163
@user_is_authorized(Engagement, Permissions.Engagement_Edit, "eid")
164164
def assign_questionnaire(request, eid, sid):
165-
survey = get_object_or_404(Answered_Survey, id=sid, engagement_id=eid)
166165
engagement = get_object_or_404(Engagement, id=eid)
166+
survey = get_object_or_404(Answered_Survey.objects.filter(engagement=engagement), id=sid)
167167

168168
form = AssignUserForm(instance=survey)
169169
if request.method == "POST":
@@ -183,8 +183,8 @@ def assign_questionnaire(request, eid, sid):
183183

184184
@user_is_authorized(Engagement, Permissions.Engagement_View, "eid")
185185
def view_questionnaire(request, eid, sid):
186-
survey = get_object_or_404(Answered_Survey, id=sid, engagement_id=eid)
187186
engagement = get_object_or_404(Engagement, id=eid)
187+
survey = get_object_or_404(Answered_Survey.objects.filter(engagement=engagement), id=sid)
188188
questions = get_answered_questions(survey=survey, read_only=True)
189189

190190
add_breadcrumb(

dojo/tools/ms_defender/parser.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,6 @@ def get_findings(self, file, test):
4242
else:
4343
input_zip = zipfile.ZipFile(file, "r")
4444

45-
max_size = 256 * 1024 * 1024 # 256 MB
46-
total_size = sum(info.file_size for info in input_zip.infolist())
47-
if total_size > max_size:
48-
msg = f"Zip file uncompressed content exceeds maximum allowed size ({total_size} > {max_size})"
49-
raise ValueError(msg)
5045
zipdata = {name: input_zip.read(name) for name in input_zip.namelist()}
5146
vulnerabilityfiles = []
5247
machinefiles = []

dojo/tools/sonarqube/parser.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,6 @@ def get_findings(self, file, test):
4242
input_zip = zipfile.ZipFile(file.name, "r")
4343
else:
4444
input_zip = zipfile.ZipFile(file, "r")
45-
max_size = 256 * 1024 * 1024 # 256 MB
46-
total_size = sum(info.file_size for info in input_zip.infolist())
47-
if total_size > max_size:
48-
msg = f"Zip file uncompressed content exceeds maximum allowed size ({total_size} > {max_size})"
49-
raise ValueError(msg)
5045
zipdata = {name: input_zip.read(name) for name in input_zip.namelist()}
5146
return SonarQubeRESTAPIZIP().get_items(zipdata, test, self.mode)
5247
parser = etree.HTMLParser()

unittests/test_permissions_audit.py

Lines changed: 10 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,8 @@
1212
8. Questionnaire cross-engagement IDOR (H1 #3571957)
1313
9. Finding Templates exposure via find_template_to_apply (H1 #3577363)
1414
10. Jira Epic BFLA - Reader cannot trigger update_jira_epic (H1 #3577193)
15-
11. Zip Bomb DoS protection in SonarQube and MS Defender parsers (H1 #3572557)
1615
"""
1716
import datetime
18-
import io
19-
import zipfile
2017

2118
from django.test import Client
2219
from django.urls import reverse
@@ -499,7 +496,7 @@ def test_edit_object_cross_product_rejected(self):
499496
url = reverse("edit_object", args=(self.product_b.id, self.tracked_file.id))
500497
response = client.get(url)
501498
# PermissionDenied raised; custom handler403 returns 400 (DD bug)
502-
self.assertIn(response.status_code, [400, 403])
499+
self.assertEqual(response.status_code, 404)
503500

504501
def test_delete_object_cross_product_rejected(self):
505502
"""Deleting an object from product A via product B's URL must be denied."""
@@ -509,7 +506,7 @@ def test_delete_object_cross_product_rejected(self):
509506
url = reverse("delete_object", args=(self.product_b.id, self.tracked_file.id))
510507
response = client.get(url)
511508
# PermissionDenied raised; custom handler403 returns 400 (DD bug)
512-
self.assertIn(response.status_code, [400, 403])
509+
self.assertEqual(response.status_code, 404)
513510

514511

515512
class TestToolProductParentCheck(DojoTestCase):
@@ -563,7 +560,7 @@ def test_edit_tool_product_cross_product_rejected(self):
563560
url = reverse("edit_tool_product", args=(self.product_b.id, self.tool_setting.id))
564561
response = client.get(url)
565562
# PermissionDenied raised; custom handler403 returns 400 (DD bug)
566-
self.assertIn(response.status_code, [400, 403])
563+
self.assertEqual(response.status_code, 404)
567564

568565
def test_delete_tool_product_cross_product_rejected(self):
569566
"""Deleting a tool setting from product A via product B's URL must be denied."""
@@ -573,7 +570,7 @@ def test_delete_tool_product_cross_product_rejected(self):
573570
url = reverse("delete_tool_product", args=(self.product_b.id, self.tool_setting.id))
574571
response = client.get(url)
575572
# PermissionDenied raised; custom handler403 returns 400 (DD bug)
576-
self.assertIn(response.status_code, [400, 403])
573+
self.assertEqual(response.status_code, 404)
577574

578575

579576
class TestRiskAcceptanceCrossEngagementIDOR(DojoTestCase):
@@ -646,7 +643,7 @@ def test_view_risk_acceptance_cross_engagement(self):
646643
self.engagement_b.id, self.risk_acceptance.id,
647644
))
648645
response = client.get(url)
649-
self.assertIn(response.status_code, [400, 403])
646+
self.assertEqual(response.status_code, 404)
650647

651648
def test_edit_risk_acceptance_cross_engagement(self):
652649
"""Editing a risk acceptance via a different engagement's URL must be denied."""
@@ -655,7 +652,7 @@ def test_edit_risk_acceptance_cross_engagement(self):
655652
self.engagement_b.id, self.risk_acceptance.id,
656653
))
657654
response = client.get(url)
658-
self.assertIn(response.status_code, [400, 403])
655+
self.assertEqual(response.status_code, 404)
659656

660657
def test_expire_risk_acceptance_cross_engagement(self):
661658
"""Expiring a risk acceptance via a different engagement's URL must be denied."""
@@ -664,7 +661,7 @@ def test_expire_risk_acceptance_cross_engagement(self):
664661
self.engagement_b.id, self.risk_acceptance.id,
665662
))
666663
response = client.get(url)
667-
self.assertIn(response.status_code, [400, 403])
664+
self.assertEqual(response.status_code, 404)
668665

669666
def test_reinstate_risk_acceptance_cross_engagement(self):
670667
"""Reinstating a risk acceptance via a different engagement's URL must be denied."""
@@ -673,7 +670,7 @@ def test_reinstate_risk_acceptance_cross_engagement(self):
673670
self.engagement_b.id, self.risk_acceptance.id,
674671
))
675672
response = client.get(url)
676-
self.assertIn(response.status_code, [400, 403])
673+
self.assertEqual(response.status_code, 404)
677674

678675
def test_delete_risk_acceptance_cross_engagement(self):
679676
"""Deleting a risk acceptance via a different engagement's URL must be denied."""
@@ -682,7 +679,7 @@ def test_delete_risk_acceptance_cross_engagement(self):
682679
self.engagement_b.id, self.risk_acceptance.id,
683680
))
684681
response = client.get(url)
685-
self.assertIn(response.status_code, [400, 403])
682+
self.assertEqual(response.status_code, 404)
686683

687684
def test_view_risk_acceptance_same_engagement(self):
688685
"""Viewing a risk acceptance via the correct engagement's URL should work."""
@@ -924,7 +921,7 @@ def test_product_writer_cannot_access_find_template(self):
924921
url = reverse("find_template_to_apply", args=(self.finding.id,))
925922
response = client.get(url)
926923
# PermissionDenied raised; custom handler403 returns 400 (DD bug)
927-
self.assertIn(response.status_code, [400, 403])
924+
self.assertEqual(response.status_code, 404)
928925

929926
def test_superuser_can_access_find_template(self):
930927
"""Superuser (implicit global permission) should be able to access."""
@@ -997,67 +994,3 @@ def test_writer_allowed_update_jira_epic(self):
997994
# Writer has Engagement_Edit, so should pass permission check.
998995
# May get 400/500 from Jira integration, but NOT 403.
999996
self.assertNotEqual(response.status_code, 403)
1000-
1001-
1002-
class TestZipBombProtection(DojoTestCase):
1003-
"""H1 #3572557: SonarQube and MS Defender parsers must reject zip files
1004-
whose uncompressed content exceeds the size limit."""
1005-
1006-
def _make_zip(self, inner_name, content=b"small content"):
1007-
"""Create a simple zip file."""
1008-
buf = io.BytesIO()
1009-
with zipfile.ZipFile(buf, "w", zipfile.ZIP_STORED) as zf:
1010-
zf.writestr(inner_name, content)
1011-
buf.seek(0)
1012-
buf.name = "test.zip"
1013-
return buf
1014-
1015-
def test_sonarqube_parser_rejects_oversized_zip(self):
1016-
"""SonarQube parser should raise ValueError for oversized zip."""
1017-
from unittest.mock import patch
1018-
1019-
from dojo.tools.sonarqube.parser import SonarQubeParser
1020-
1021-
test_zip = self._make_zip("sonar-report.html")
1022-
parser = SonarQubeParser()
1023-
1024-
# Mock infolist to report a huge file_size (simulating a zip bomb)
1025-
fake_info = zipfile.ZipInfo("sonar-report.html")
1026-
fake_info.file_size = 512 * 1024 * 1024 # 512 MB
1027-
1028-
with patch.object(zipfile.ZipFile, "infolist", return_value=[fake_info]):
1029-
with self.assertRaises(ValueError) as ctx:
1030-
parser.get_findings(test_zip, None)
1031-
self.assertIn("exceeds maximum allowed size", str(ctx.exception))
1032-
1033-
def test_ms_defender_parser_rejects_oversized_zip(self):
1034-
"""MS Defender parser should raise ValueError for oversized zip."""
1035-
from unittest.mock import patch
1036-
1037-
from dojo.tools.ms_defender.parser import MSDefenderParser
1038-
1039-
test_zip = self._make_zip("vulnerabilities/vuln1.json")
1040-
parser = MSDefenderParser()
1041-
1042-
fake_info = zipfile.ZipInfo("vulnerabilities/vuln1.json")
1043-
fake_info.file_size = 512 * 1024 * 1024
1044-
1045-
with patch.object(zipfile.ZipFile, "infolist", return_value=[fake_info]):
1046-
with self.assertRaises(ValueError) as ctx:
1047-
parser.get_findings(test_zip, None)
1048-
self.assertIn("exceeds maximum allowed size", str(ctx.exception))
1049-
1050-
def test_sonarqube_parser_accepts_normal_zip(self):
1051-
"""SonarQube parser should accept a reasonably sized zip."""
1052-
from dojo.tools.sonarqube.parser import SonarQubeParser
1053-
1054-
test_zip = self._make_zip(
1055-
"sonar-report.html", b"<html><body>report</body></html>",
1056-
)
1057-
parser = SonarQubeParser()
1058-
1059-
# Should not raise ValueError (may raise other errors from parsing)
1060-
try:
1061-
parser.get_findings(test_zip, None)
1062-
except ValueError:
1063-
self.fail("SonarQubeParser raised ValueError on a normal-sized zip")

0 commit comments

Comments
 (0)