From da06786b02ea3173981cba609bfd6997aa9b639e Mon Sep 17 00:00:00 2001 From: Jino Tesauro Date: Fri, 3 Apr 2026 15:40:43 -0500 Subject: [PATCH 1/4] change-to-moving-engagements --- dojo/api_v2/serializers.py | 26 +++++++++++++++++++++++++- dojo/api_v2/views.py | 5 +++++ dojo/models.py | 2 +- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index 02926134d41..f8ec2383220 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -1119,7 +1119,7 @@ class Meta: exclude = ("inherited_tags",) def validate(self, data): - if self.context["request"].method == "POST": + if data.get("target_start") and data.get("target_end"): if data.get("target_start") > data.get("target_end"): msg = "Your target start date exceeds your target end date" raise serializers.ValidationError(msg) @@ -1133,6 +1133,30 @@ def build_relational_field(self, field_name, relation_info): return super().build_relational_field(field_name, relation_info) +class EngagementCreateSerializer(serializers.ModelSerializer): + product = serializers.PrimaryKeyRelatedField( + queryset=Product.objects.all(), + ) + tags = TagListSerializerField(required=False) + + class Meta: + model = Engagement + exclude = ("inherited_tags",) + + def validate(self, data): + if data.get("target_start") > data.get("target_end"): + msg = "Your target start date exceeds your target end date" + raise serializers.ValidationError(msg) + return data + + def build_relational_field(self, field_name, relation_info): + if field_name == "notes": + return NoteSerializer, {"many": True, "read_only": True} + if field_name == "files": + return FileSerializer, {"many": True, "read_only": True} + return super().build_relational_field(field_name, relation_info) + + class EngagementToNotesSerializer(serializers.Serializer): engagement_id = serializers.PrimaryKeyRelatedField( queryset=Engagement.objects.all(), many=False, allow_null=True, diff --git a/dojo/api_v2/views.py b/dojo/api_v2/views.py index c77927ce666..7d39fbd01c2 100644 --- a/dojo/api_v2/views.py +++ b/dojo/api_v2/views.py @@ -450,6 +450,11 @@ class EngagementViewSet( def risk_application_model_class(self): return Engagement + def get_serializer_class(self): + if self.request and self.request.method == "POST": + return serializers.EngagementCreateSerializer + return serializers.EngagementSerializer + def destroy(self, request, *args, **kwargs): instance = self.get_object() if get_setting("ASYNC_OBJECT_DELETE"): diff --git a/dojo/models.py b/dojo/models.py index bcfb39180a4..c9023f95551 100644 --- a/dojo/models.py +++ b/dojo/models.py @@ -1526,7 +1526,7 @@ class Engagement(BaseModel): preset = models.ForeignKey(Engagement_Presets, null=True, blank=True, help_text=_("Settings and notes for performing this engagement."), on_delete=models.CASCADE) reason = models.CharField(max_length=2000, null=True, blank=True) report_type = models.ForeignKey(Report_Type, null=True, blank=True, on_delete=models.CASCADE) - product = models.ForeignKey(Product, on_delete=models.CASCADE) + product = models.ForeignKey(Product, editable=False, on_delete=models.CASCADE) active = models.BooleanField(default=True, editable=False) tracker = models.URLField(max_length=200, help_text=_("Link to epic or ticket system with changes to version."), editable=True, blank=True, null=True) test_strategy = models.URLField(editable=True, blank=True, null=True) From 4e2c14c0efe8514988aa7f5886e62cef6482de3d Mon Sep 17 00:00:00 2001 From: Jino Tesauro Date: Fri, 3 Apr 2026 16:14:39 -0500 Subject: [PATCH 2/4] fix-migration-issue: --- .../0264_alter_engagement_product_and_more.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 dojo/db_migrations/0264_alter_engagement_product_and_more.py diff --git a/dojo/db_migrations/0264_alter_engagement_product_and_more.py b/dojo/db_migrations/0264_alter_engagement_product_and_more.py new file mode 100644 index 00000000000..7752e3c96c9 --- /dev/null +++ b/dojo/db_migrations/0264_alter_engagement_product_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.12 on 2026-04-03 20:48 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dojo', '0263_language_type_unique_language'), + ] + + operations = [ + migrations.AlterField( + model_name='engagement', + name='product', + field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, to='dojo.product'), + ), + migrations.AlterField( + model_name='engagementevent', + name='product', + field=models.ForeignKey(db_constraint=False, db_index=False, editable=False, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', related_query_name='+', to='dojo.product'), + ), + ] From dfc390c8ebff46643e5c2d3554535a119af66b52 Mon Sep 17 00:00:00 2001 From: Jino Tesauro Date: Fri, 3 Apr 2026 16:23:31 -0500 Subject: [PATCH 3/4] change-reimport --- dojo/api_v2/permissions.py | 16 ++++++++++++++++ dojo/api_v2/serializers.py | 3 +++ 2 files changed, 19 insertions(+) diff --git a/dojo/api_v2/permissions.py b/dojo/api_v2/permissions.py index 807bbcf7b2a..e4b5234277a 100644 --- a/dojo/api_v2/permissions.py +++ b/dojo/api_v2/permissions.py @@ -469,6 +469,14 @@ def has_permission(self, request, view): converted_dict["product_type"] = auto_create.get_target_product_type_if_exists(**converted_dict) converted_dict["product"] = auto_create.get_target_product_if_exists(**converted_dict) converted_dict["engagement"] = auto_create.get_target_engagement_if_exists(**converted_dict) + # Guard: if an engagement was resolved by ID and product_name also + # resolved to a product, they must agree. An attacker could otherwise + # supply an engagement ID they own (passing the permission check below) + # while pointing product_name at a product they cannot access. + if (converted_dict.get("engagement") and converted_dict.get("product") and + converted_dict["engagement"].product != converted_dict["product"]): + msg = "The specified engagement does not belong to the specified product." + raise serializers.ValidationError(msg) except (ValueError, TypeError) as e: # Raise an explicit drf exception here raise ValidationError(e) @@ -769,6 +777,14 @@ def has_permission(self, request, view): converted_dict["product"] = auto_create.get_target_product_if_exists(**converted_dict) converted_dict["engagement"] = auto_create.get_target_engagement_if_exists(**converted_dict) converted_dict["test"] = auto_create.get_target_test_if_exists(**converted_dict) + # Guard: if an engagement was resolved by ID and product_name also + # resolved to a product, they must agree. An attacker could otherwise + # supply an engagement ID they own (passing the permission check below) + # while pointing product_name at a product they cannot access. + if (converted_dict.get("engagement") and converted_dict.get("product") and + converted_dict["engagement"].product != converted_dict["product"]): + msg = "The specified engagement does not belong to the specified product." + raise serializers.ValidationError(msg) except (ValueError, TypeError) as e: # Raise an explicit drf exception here raise ValidationError(e) diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index f8ec2383220..c405ed22c07 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -2662,6 +2662,9 @@ class ReImportScanSerializer(CommonImportScanSerializer): scan_type = serializers.ChoiceField( choices=get_choices_sorted(), required=True, ) + engagement = serializers.PrimaryKeyRelatedField( + required=False, queryset=Engagement.objects.all(), + ) test = serializers.PrimaryKeyRelatedField( required=False, queryset=Test.objects.all(), ) From 8ecbd1f7dd815217685cbfa8b019d12dd5ceea10 Mon Sep 17 00:00:00 2001 From: Jino Tesauro Date: Fri, 3 Apr 2026 16:54:59 -0500 Subject: [PATCH 4/4] changes so unittests pass --- dojo/api_v2/serializers.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/dojo/api_v2/serializers.py b/dojo/api_v2/serializers.py index c405ed22c07..f8ec2383220 100644 --- a/dojo/api_v2/serializers.py +++ b/dojo/api_v2/serializers.py @@ -2662,9 +2662,6 @@ class ReImportScanSerializer(CommonImportScanSerializer): scan_type = serializers.ChoiceField( choices=get_choices_sorted(), required=True, ) - engagement = serializers.PrimaryKeyRelatedField( - required=False, queryset=Engagement.objects.all(), - ) test = serializers.PrimaryKeyRelatedField( required=False, queryset=Test.objects.all(), )