Skip to content

Fix CVSS 4.0 score when a lower macrovector ties the current score - #21

Open
bjhijmans wants to merge 1 commit into
Rootshell-Security:developfrom
bjhijmans:develop
Open

Fix CVSS 4.0 score when a lower macrovector ties the current score#21
bjhijmans wants to merge 1 commit into
Rootshell-Security:developfrom
bjhijmans:develop

Conversation

@bjhijmans

Copy link
Copy Markdown

Fix CVSS 4.0 score bias when a lower macrovector ties the current score

Closes #20

Summary

CVSS 4.0 vectors can be scored ~0.1 too low whenever an equivalence class (EQ) has a defined lower macrovector that happens to score the same as the current macrovector. The bug is in Cvss40Calculator::calculateMeanDistance().

Reproduction

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

Root cause

The final score is initialValue − meanDistance, where the mean distance sums per-EQ contributions and divides by the number of EQs that have a defined lower macrovector.

calculateMeanDistance() selected which EQs count with a truthy check on the float available distance:

if ($availableDistance->eqThree) { $existingLower++; ... }

Cvss4Distance types every field as a plain, non-nullable float defaulting to 0.0, and calculateAvailableDistance() only overwrites a field when the lower macrovector exists. So two different states collapse to the same falsy 0.0:

  • no lower macrovector (should be excluded), and
  • lower macrovector exists but scores the same as the current one, giving an available distance of exactly 0.0 (should be included).

Excluding the second case shrinks the divisor, inflates the mean distance, and biases the score downward.

For the vector above (macrovector 111110, initial 5.7):

EQ lower vector defined? avail. dist contribution
1 211110 yes (4.8) 0.9 0.45
2 121110 no skipped
3 111111 yes (5.7) 0.0 0.0
4 111210 yes (5.7) 0.0 0.0
5 111120 yes (4.7) 1.0 0.0
  • Reference: divisor 4 → mean 0.1125 → 5.7 − 0.1125 = 5.5875 → 5.6
  • Before fix: EQ3/EQ4 dropped → divisor 2 → mean 0.225 → 5.475 → 5.5

Fix

Gate each EQ on whether its lower macrovector is defined (!is_null($lowerValues[n])) instead of the truthiness of the distance, matching the FIRST reference. The $lowerVectorValues array — whose elements are ?float (lookupMicroVector() returns null for an undefined macrovector) — is passed into calculateMeanDistance(). Normalized-severity contributions still use $availableDistance and correctly remain 0.0 in the tie case, so only the divisor changes.

Added a regression test for the affected string.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Small discrepency with first.org's calculator

1 participant