From 918768107524f8cd21abe3e059d8cc34104c37f5 Mon Sep 17 00:00:00 2001 From: Sam Vader Date: Mon, 21 Sep 2026 12:56:46 -0500 Subject: [PATCH] Narrow the scan-import product type conflict error The message named the stored Product Type of the resolved product. That value reaches callers who hold no grant on it, because the import permission classes resolve the product before any authorization call runs. Report only the value the caller supplied. Refs H1 #4032888, story 15579. --- dojo/importers/auto_create_context.py | 1 - .../test_apiv2_import_error_disclosure.py | 72 +++++++++++++++++++ unittests/test_importers_importer.py | 2 - 3 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 unittests/test_apiv2_import_error_disclosure.py diff --git a/dojo/importers/auto_create_context.py b/dojo/importers/auto_create_context.py index fb3e06d7ae8..9a7cca77804 100644 --- a/dojo/importers/auto_create_context.py +++ b/dojo/importers/auto_create_context.py @@ -141,7 +141,6 @@ def get_target_product_if_exists( if product_type_name and product.prod_type.name != product_type_name: msg = ( "The fetched product has a conflict with the supplied product type name: " - f"existing product type name - {product.prod_type.name} vs " f"supplied product type name - {product_type_name}" ) raise ValueError(msg) diff --git a/unittests/test_apiv2_import_error_disclosure.py b/unittests/test_apiv2_import_error_disclosure.py new file mode 100644 index 00000000000..d62b0ec56e4 --- /dev/null +++ b/unittests/test_apiv2_import_error_disclosure.py @@ -0,0 +1,72 @@ +""" +Regression tests for information disclosure through the scan-import permission classes. + +The import permission classes resolve caller-supplied names against unscoped managers +before any authorization call runs. A mismatch between the supplied product type name +and the stored one must not put the stored name into the error body, because the caller +reaching that branch may hold no grant on the resolved product at all. +""" +from rest_framework.authtoken.models import Token +from rest_framework.test import APIClient + +from dojo.models import Dojo_User, Product, Product_Type + +from .dojo_test_case import DojoTestCase + +SECRET_PRODUCT_TYPE_NAME = "Confidential Tenant Name Sentinel" + + +class TestImportErrorDisclosure(DojoTestCase): + + @classmethod + def setUpTestData(cls): + cls.product_type = Product_Type.objects.create(name=SECRET_PRODUCT_TYPE_NAME) + cls.product = Product.objects.create( + name="Import Disclosure Victim Product", + description="victim", + prod_type=cls.product_type, + ) + # No Product_Member, no Product_Type_Member, no staff, no superuser. + cls.outsider = Dojo_User.objects.create_user( + username="import_disclosure_outsider", is_active=True, + ) + cls.token = Token.objects.create(user=cls.outsider) + + def _client(self): + client = APIClient() + client.credentials(HTTP_AUTHORIZATION=f"Token {self.token.key}") + return client + + def _payload(self): + return { + "scan_type": "Generic Findings Import", + "product_name": self.product.name, + "product_type_name": "a name the caller guessed wrong", + "engagement_name": "an engagement that does not exist", + } + + def _assert_no_disclosure(self, response): + self.assertEqual(response.status_code, 400) + self.assertNotIn(SECRET_PRODUCT_TYPE_NAME, response.content.decode()) + + def test_outsider_cannot_read_the_product_type(self): + response = self._client().get(f"/api/v2/product_types/{self.product_type.id}/") + self.assertEqual(response.status_code, 404) + + def test_import_scan_does_not_disclose_product_type_name(self): + self._assert_no_disclosure( + self._client().post("/api/v2/import-scan/", self._payload()), + ) + + def test_reimport_scan_does_not_disclose_product_type_name(self): + self._assert_no_disclosure( + self._client().post("/api/v2/reimport-scan/", self._payload()), + ) + + def test_endpoint_meta_import_does_not_disclose_product_type_name(self): + payload = self._payload() + payload.pop("scan_type") + payload.pop("engagement_name") + self._assert_no_disclosure( + self._client().post("/api/v2/endpoint_meta_import/", payload), + ) diff --git a/unittests/test_importers_importer.py b/unittests/test_importers_importer.py index 956bf6b7e46..c0575e8706e 100644 --- a/unittests/test_importers_importer.py +++ b/unittests/test_importers_importer.py @@ -906,7 +906,6 @@ def test_import_with_invalid_parameters(self): engagement=None, product_type_name=another_product_type_name, product_name=PRODUCT_NAME_DEFAULT, engagement_name="valentijn", expected_http_status_code=400) self.assertEqual(import0, [( "The fetched product has a conflict with the supplied product type name: " - f"existing product type name - {PRODUCT_TYPE_NAME_DEFAULT} vs " f"supplied product type name - {another_product_type_name}" )]) @@ -1108,7 +1107,6 @@ def test_reimport_with_invalid_parameters(self): engagement=None, product_type_name=another_product_type_name, product_name=PRODUCT_NAME_DEFAULT, engagement_name="valentijn", expected_http_status_code=400) self.assertEqual(import0, [( "The fetched product has a conflict with the supplied product type name: " - f"existing product type name - {PRODUCT_TYPE_NAME_DEFAULT} vs " f"supplied product type name - {another_product_type_name}" )])