From 0a3a7834290e98ee277ca37a481f4837eca85982 Mon Sep 17 00:00:00 2001 From: Bart Hijmans Date: Mon, 6 Jul 2026 16:43:55 +0200 Subject: [PATCH] Fix CVSS 4.0 score when a lower macrovector ties the current score MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit calculateMeanDistance() decided whether an equivalence class counted toward the mean-distance divisor with a truthy check on its available distance (`if ($availableDistance->eqN)`). Because Cvss4Distance defaults every field to 0.0, an EQ whose lower macrovector exists but scores identically to the current macrovector — a legitimate available distance of exactly 0.0 — was indistinguishable from an EQ with no lower macrovector at all, and was wrongly excluded from the divisor. Dropping such an EQ shrinks `existingLower`, inflates the mean distance, and biases the final score downward. For example, CVSS:4.0/AV:N/AC:L/AT:P/PR:H/UI:P/VC:L/VI:H/VA:L/SC:H/SI:H/SA:H/E:P/MAV:L/MAC:H/MAT:P/MPR:N/MUI:P scored 5.5 instead of 5.6 (EQ3 and EQ4 each have a valid lower macrovector at the same 5.7 score). Gate each EQ on whether its lower macrovector is defined (`!is_null($lowerValues[n])`) — mirroring the FIRST reference implementation — by threading the already-computed $lowerVectorValues into calculateMeanDistance(). The normalized-severity contributions still use $availableDistance and remain 0.0 in the tie case. Add the affected vector to the CvssTest data provider as a regression test. --- src/Calculators/Cvss40Calculator.php | 17 ++++++++++------- tests/CvssTest.php | 2 ++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Calculators/Cvss40Calculator.php b/src/Calculators/Cvss40Calculator.php index 64b09b4..b59c4a2 100644 --- a/src/Calculators/Cvss40Calculator.php +++ b/src/Calculators/Cvss40Calculator.php @@ -383,7 +383,7 @@ public function calculateBaseScore(CvssObject $cvssObject): float $severityDistance = $this->calculateSeverityDistance($cvssObject, $maxVector); $availableDistance = $this->calculateAvailableDistance($initialValue, $lowerVectorValues); - $finalValue = $initialValue - $this->calculateMeanDistance($cvssObject, $severityDistance, $availableDistance); + $finalValue = $initialValue - $this->calculateMeanDistance($cvssObject, $severityDistance, $availableDistance, $lowerVectorValues); if ($finalValue < 0.0) { return 0.0; @@ -519,12 +519,15 @@ private function calculateSeverityDistance(Cvss4Object $cvssObject, Cvss4Object ); } - private function calculateMeanDistance(Cvss4Object $cvssObject, Cvss4Distance $severityDistance, Cvss4Distance $availableDistance): float + /** + * @param float[]|null[] $lowerValues + */ + private function calculateMeanDistance(Cvss4Object $cvssObject, Cvss4Distance $severityDistance, Cvss4Distance $availableDistance, array $lowerValues): float { $normalisedSeverity = new Cvss4Distance(); $existingLower = 0; - if ($availableDistance->eqOne) { + if (!is_null($lowerValues[1])) { $existingLower++; if (!isset($this->maxSeverity[1][$cvssObject->eq1]) || !is_int($this->maxSeverity[1][$cvssObject->eq1])) { @@ -535,7 +538,7 @@ private function calculateMeanDistance(Cvss4Object $cvssObject, Cvss4Distance $s $normalisedSeverity->eqOne = $availableDistance->eqOne * ($severityDistance->eqOne / $maxSeverityOne); } - if ($availableDistance->eqTwo) { + if (!is_null($lowerValues[2])) { $existingLower++; if (!isset($this->maxSeverity[2][$cvssObject->eq2]) || !is_int($this->maxSeverity[2][$cvssObject->eq2])) { @@ -546,7 +549,7 @@ private function calculateMeanDistance(Cvss4Object $cvssObject, Cvss4Distance $s $normalisedSeverity->eqTwo = $availableDistance->eqTwo * ($severityDistance->eqTwo / $maxSeverityTwo); } - if ($availableDistance->eqThree) { + if (!is_null($lowerValues[3])) { $existingLower++; if (!isset($this->maxSeverity[3][$cvssObject->eq3][$cvssObject->eq6]) || !is_int($this->maxSeverity[3][$cvssObject->eq3][$cvssObject->eq6])) { @@ -557,7 +560,7 @@ private function calculateMeanDistance(Cvss4Object $cvssObject, Cvss4Distance $s $normalisedSeverity->eqThree = $availableDistance->eqThree * ($severityDistance->eqThree / $maxSeverityThree); } - if ($availableDistance->eqFour) { + if (!is_null($lowerValues[4])) { $existingLower++; if (!isset($this->maxSeverity[4][$cvssObject->eq4]) || !is_int($this->maxSeverity[4][$cvssObject->eq4])) { @@ -568,7 +571,7 @@ private function calculateMeanDistance(Cvss4Object $cvssObject, Cvss4Distance $s $normalisedSeverity->eqFour = $availableDistance->eqFour * ($severityDistance->eqFour / $maxSeverityFour); } - if ($availableDistance->eqFive) { + if (!is_null($lowerValues[5])) { $existingLower++; $normalisedSeverity->eqFive = 0; } diff --git a/tests/CvssTest.php b/tests/CvssTest.php index 23bad38..ef365e6 100644 --- a/tests/CvssTest.php +++ b/tests/CvssTest.php @@ -48,6 +48,8 @@ public static function vectorProvider(): array ['CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N/S:N/AU:N/R:A/V:D/RE:L/U:Clear', 6.9, 6.9, 6.9], ['CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X', 7.5, 7.5, 7.5], ['CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N', 0, 0, 0], + // Regression test for #20 + ['CVSS:4.0/AV:N/AC:L/AT:P/PR:H/UI:P/VC:L/VI:H/VA:L/SC:H/SI:H/SA:H/E:P/MAV:L/MAC:H/MAT:P/MPR:N/MUI:P', 5.6, 5.6, 5.6], ['CVSS:3.1/AV:A/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H', 8.0, 8.0, 8.0],