From 8849f27e5c8d0d9cf7051fbeccb42604c9ac0069 Mon Sep 17 00:00:00 2001 From: Shubham Rai Date: Fri, 9 Oct 2026 13:27:13 +0530 Subject: [PATCH] fix(api_v3): validate PURLs in packages and advisories query serializers In api_v3, passing malformed or invalid Package URLs to POST /api/v3/packages or POST /api/v3/advisories either causes an unhandled ValueError (500 Internal Server Error) when details/ignore_qualifiers_subpath is set, or silently returns empty results. Add validate_purls validation methods to both PackageQuerySerializer and AdvisoryQuerySerializer using PackageURL.from_string, ensuring consistent HTTP 400 Bad Request responses with descriptive validation error messages for malformed PURLs across all v3 query endpoints. Add test cases in test_api_v3 covering invalid PURL submissions for both endpoints. Signed-off-by: Shubham Rai --- vulnerabilities/api_v3.py | 16 ++++++++++++++ vulnerabilities/tests/test_api_v3.py | 32 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/vulnerabilities/api_v3.py b/vulnerabilities/api_v3.py index 75653ca2b..45461c850 100644 --- a/vulnerabilities/api_v3.py +++ b/vulnerabilities/api_v3.py @@ -57,6 +57,14 @@ class PackageQuerySerializer(serializers.Serializer): max_advisories = serializers.IntegerField(default=100, min_value=1, max_value=10000) reachability = serializers.BooleanField(default=False) + def validate_purls(self, purls): + for purl in purls: + try: + PackageURL.from_string(purl) + except ValueError as e: + raise serializers.ValidationError(str(e)) + return purls + def validate(self, data): if not data["purls"]: if data["details"] or data["ignore_qualifiers_subpath"]: @@ -81,6 +89,14 @@ class AdvisoryQuerySerializer(serializers.Serializer): default=list, ) + def validate_purls(self, purls): + for purl in purls: + try: + PackageURL.from_string(purl) + except ValueError as e: + raise serializers.ValidationError(str(e)) + return purls + def validate(self, data): if not data["purls"]: raise serializers.ValidationError("purls is required") diff --git a/vulnerabilities/tests/test_api_v3.py b/vulnerabilities/tests/test_api_v3.py index c00c21354..8e3975c14 100644 --- a/vulnerabilities/tests/test_api_v3.py +++ b/vulnerabilities/tests/test_api_v3.py @@ -140,6 +140,38 @@ def test_packages_post_with_details_fixing_vulnerabilities(self): self.assertEqual(fixing_vulnerability["datasource_id"], "ghsa") self.assertEqual(pkg["next_non_vulnerable_version"], "2.0.0") + def test_packages_post_with_invalid_purl(self): + url = reverse("package-v3-list") + + for ignore_qualifiers_subpath in (False, True): + with self.subTest(ignore_qualifiers_subpath=ignore_qualifiers_subpath): + response = self.client.post( + url, + data={ + "purls": ["not-a-purl"], + "details": True, + "ignore_qualifiers_subpath": ignore_qualifiers_subpath, + }, + format="json", + HTTP_USER_AGENT="VCIO_API_AGENT", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("purls", response.data) + + def test_advisories_post_with_invalid_purl(self): + url = reverse("advisory-v3-list") + + response = self.client.post( + url, + data={"purls": ["invalid-purl-scheme"]}, + format="json", + HTTP_USER_AGENT="VCIO_API_AGENT", + ) + + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("purls", response.data) + def test_advisories_post(self): url = reverse("advisory-v3-list")