From a95a3e066f7b913ea3827402016f1b3312680feb Mon Sep 17 00:00:00 2001 From: Sam Vader Date: Fri, 24 Jul 2026 16:19:46 -0500 Subject: [PATCH 1/2] Restrict JIRA finding-mapping project field to authorized projects The project relation on the JIRA finding-mapping API is writable but was resolved from the full manager, so a create or update could reference a project the caller is not authorized for. Scope the field's queryset to the caller's authorized projects and add a regression test for create and update. --- dojo/jira/api/serializers.py | 15 ++++++++ unittests/test_rest_framework.py | 63 ++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/dojo/jira/api/serializers.py b/dojo/jira/api/serializers.py index a1270ed885c..e23817317e6 100644 --- a/dojo/jira/api/serializers.py +++ b/dojo/jira/api/serializers.py @@ -4,6 +4,7 @@ from rest_framework import serializers from dojo.jira import services as jira_services +from dojo.jira.queries import get_authorized_jira_projects from dojo.models import ( JIRA_Instance, JIRA_Issue, @@ -18,6 +19,20 @@ class Meta: model = JIRA_Issue fields = "__all__" + def get_fields(self): + fields = super().get_fields() + # The jira_project relation is writable, so restrict the choosable + # projects to the ones the caller is allowed to edit. Otherwise the + # field would resolve any project id from the full manager on write. + jira_project_field = fields.get("jira_project") + user = getattr(self.context.get("request"), "user", None) + # Only narrow for a real authenticated request. An anonymous/absent + # user (e.g. OpenAPI schema generation) keeps the default queryset and + # must not be handed to the authorization filter. + if jira_project_field is not None and user is not None and not user.is_anonymous: + jira_project_field.queryset = get_authorized_jira_projects("edit", user=user) + return fields + def get_url(self, obj) -> str: return jira_services.get_issue_url(obj) diff --git a/unittests/test_rest_framework.py b/unittests/test_rest_framework.py index 892b6533dd4..9f01f909586 100644 --- a/unittests/test_rest_framework.py +++ b/unittests/test_rest_framework.py @@ -72,6 +72,7 @@ App_Analysis, BurpRawRequestResponse, Development_Environment, + Dojo_User, DojoMeta, Endpoint, Endpoint_Status, @@ -2515,6 +2516,68 @@ def __init__(self, *args, **kwargs): BaseClass.RESTEndpointTest.__init__(self, *args, **kwargs) +class JiraIssueProjectScopingTest(DojoAPITestCase): + + """ + The jira project on a jira finding mapping is writable, so a caller may only + reference a project they are allowed to edit. A member of one product must + not be able to attach or repoint a mapping to another product's project. + """ + + fixtures = ["dojo_testdata.json"] + + def setUp(self): + super().setUp() + pt_a = self.create_product_type("scoping-pt-a") + pt_b = self.create_product_type("scoping-pt-b") + self.product_a = self.create_product("scoping-prod-a", prod_type=pt_a) + self.product_b = self.create_product("scoping-prod-b", prod_type=pt_b) + + self.user = Dojo_User.objects.create(username="jira-scoping-user", is_staff=False, is_superuser=False) + self.product_a.authorized_users.add(self.user) + + Test_Type.objects.get_or_create(name="scoping-tt") + engagement = self.create_engagement("scoping-eng", self.product_a) + test = self.create_test(engagement=engagement, scan_type="scoping-tt", title="scoping-test") + self.finding = Finding.objects.create( + title="scoping-finding", test=test, severity="Low", + numerical_severity="S3", reporter=self.user, + ) + + # jira_instance is nullable; the scoping check only needs the projects to exist. + self.project_a = JIRA_Project.objects.create(product=self.product_a, project_key="AAA") + self.project_b = JIRA_Project.objects.create(product=self.product_b, project_key="BBB") + + token = Token.objects.create(user=self.user) + self.client = APIClient() + self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key) + self.url = reverse("jira_issue-list") + + def test_create_rejects_unauthorized_jira_project(self): + before = JIRA_Issue.objects.count() + response = self.client.post(self.url, { + "jira_project": self.project_b.id, "jira_id": "1", "jira_key": "BBB-1", + "finding": self.finding.id, + }, format="json") + self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code, response.content[:500]) + self.assertEqual(before, JIRA_Issue.objects.count()) + + def test_create_allows_authorized_jira_project(self): + response = self.client.post(self.url, { + "jira_project": self.project_a.id, "jira_id": "1", "jira_key": "AAA-1", + "finding": self.finding.id, + }, format="json") + self.assertEqual(status.HTTP_201_CREATED, response.status_code, response.content[:500]) + self.assertEqual(self.project_a.id, JIRA_Issue.objects.get(id=response.data["id"]).jira_project_id) + + def test_update_rejects_unauthorized_jira_project(self): + issue = JIRA_Issue.objects.create(jira_project=self.project_a, jira_id="1", jira_key="AAA-1", finding=self.finding) + response = self.client.patch(f"{self.url}{issue.id}/", {"jira_project": self.project_b.id}, format="json") + self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code, response.content[:500]) + issue.refresh_from_db() + self.assertEqual(self.project_a.id, issue.jira_project_id) + + @versioned_fixtures class JiraProjectTest(BaseClass.BaseClassTest): fixtures = ["dojo_testdata.json"] From e9634de4e26bad8f546eb5d0566e2cccb8bf6e25 Mon Sep 17 00:00:00 2001 From: Sam Vader Date: Fri, 24 Jul 2026 16:53:23 -0500 Subject: [PATCH 2/2] Use versioned fixtures in the JIRA finding-mapping scoping test The new test class loaded dojo_testdata.json directly, which fails to install when the locations feature flag is on. Apply the existing versioned_fixtures decorator so it picks the matching fixture, same as the neighbouring JIRA test classes. --- unittests/test_rest_framework.py | 1 + 1 file changed, 1 insertion(+) diff --git a/unittests/test_rest_framework.py b/unittests/test_rest_framework.py index 9f01f909586..0177fc32845 100644 --- a/unittests/test_rest_framework.py +++ b/unittests/test_rest_framework.py @@ -2516,6 +2516,7 @@ def __init__(self, *args, **kwargs): BaseClass.RESTEndpointTest.__init__(self, *args, **kwargs) +@versioned_fixtures class JiraIssueProjectScopingTest(DojoAPITestCase): """