CVSS Scoring & Reporting
A finding is only useful if the right people understand how serious it is and how to fix it. Two testers who describe the same SQL injection as "bad" and "very bad" do not help anyone. Security teams therefore use a shared scoring system, CVSS, and a predictable report structure.
In this lesson you will learn how CVSS scores and vector strings work in versions 4.0 and 3.1, why a CVSS score is not the same as risk, which other sources (CVE, CWE, EPSS and KEV) add context, and how to write a finding that a developer can fix and a manager can approve.
What CVSS is
The Common Vulnerability Scoring System (CVSS) is maintained by FIRST, the global forum of incident response teams. It gives a vulnerability a score from 0.0 to 10.0 and a vector string that records every choice behind the score. CVSS v4.0 was published on 1 November 2023. CVSS v3.1 is still very common, and public databases such as NIST's National Vulnerability Database show scores in both versions.
Both versions use the same severity ratings:
| Score | Rating |
|---|---|
| 0.0 | None |
| 0.1 to 3.9 | Low |
| 4.0 to 6.9 | Medium |
| 7.0 to 8.9 | High |
| 9.0 to 10.0 | Critical |
def rating(score):
if score == 0:
return "None"
if score < 4.0:
return "Low"
if score < 7.0:
return "Medium"
if score < 9.0:
return "High"
return "Critical"
for s in [0.0, 3.9, 4.0, 6.9, 7.5, 9.3, 10.0]:
print(f"{s:>4} {rating(s)}")0.0 None 3.9 Low 4.0 Medium 6.9 Medium 7.5 High 9.3 Critical 10.0 Critical
CVSS v4.0 base metrics
CVSS v4.0 has four metric groups: Base (the vulnerability itself, which never changes), Threat (whether exploitation is happening), Environmental (your own setup and needs) and Supplemental (extra information that does not change the score). A score is labelled by the groups used: CVSS-B for base only, CVSS-BT for base plus threat, and so on.
The base metrics describe how the vulnerability is exploited and what it affects:
| Metric | Abbreviation | Values |
|---|---|---|
| Attack Vector | AV | Network, Adjacent, Local, Physical |
| Attack Complexity | AC | Low, High |
| Attack Requirements | AT | None, Present (conditions outside the attacker's control) |
| Privileges Required | PR | None, Low, High |
| User Interaction | UI | None, Passive, Active |
| Vulnerable system impact | VC, VI, VA | High, Low, None for confidentiality, integrity, availability |
| Subsequent system impact | SC, SI, SA | High, Low, None for other systems affected afterwards |
A SQL injection in a public login page that lets anyone read and change the whole database, with no effect on other systems, has this vector:
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:NFIRST's official calculator gives this vector a score of 9.3 (Critical). CVSS v4.0 scores are computed from lookup tables, not a short formula, so always use the official calculator on the FIRST website rather than doing it by hand. A program can still read a vector and explain it:
NAMES = {
"AV": ("Attack Vector", {"N": "Network", "A": "Adjacent", "L": "Local", "P": "Physical"}),
"AC": ("Attack Complexity", {"L": "Low", "H": "High"}),
"AT": ("Attack Requirements", {"N": "None", "P": "Present"}),
"PR": ("Privileges Required", {"N": "None", "L": "Low", "H": "High"}),
"UI": ("User Interaction", {"N": "None", "P": "Passive", "A": "Active"}),
}
IMPACT = {"H": "High", "L": "Low", "N": "None"}
def explain(vector):
version, *parts = vector.split("/")
print("Version:", version.split(":")[1])
for part in parts:
key, value = part.split(":")
if key in NAMES:
label, values = NAMES[key]
print(f" {label:<20} {values[value]}")
else:
print(f" {key + ' impact':<20} {IMPACT[value]}")
explain("CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N")Version: 4.0 Attack Vector Network Attack Complexity Low Attack Requirements None Privileges Required Low User Interaction None VC impact High VI impact Low VA impact None SC impact None SI impact None SA impact None
This vector describes a logged-in user (PR:L) who can read other users' data (VC:H) and change a little of it (VI:L), a typical broken access control finding.
Severity is not risk
CVSS measures how severe a vulnerability is in general. Risk depends on your situation. A 9.8 in an internal test server with no data may matter less than a 6.5 in your payment page. Good reports add context from:
- CVE (Common Vulnerabilities and Exposures): a unique ID for a publicly known vulnerability, such as
CVE-2021-44228(Log4Shell). CERT-In is a CVE Numbering Authority for products made in India. - CWE (Common Weakness Enumeration): the type of weakness, such as CWE-89 (SQL injection) or CWE-79 (cross-site scripting). Findings in custom code usually have a CWE but no CVE.
- EPSS (Exploit Prediction Scoring System, also from FIRST): the estimated probability that a CVE will be exploited in the next 30 days.
- KEV: the US CISA catalogue of Known Exploited Vulnerabilities, a list of CVEs that attackers are using right now.
A sensible rule: fix anything that is in KEV or actively exploited first, then high CVSS findings on important assets.
Writing a finding
Every finding in a report should answer the same questions in the same order:
Title: SQL injection in product search (DVWA, port 4280)
Severity: Critical, CVSS-B 9.3
Vector: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N
Weakness: CWE-89
Asset: http://192.168.56.101:4280/vulnerabilities/sqli/ (lab)
Description: The id parameter is joined directly into an SQL query.
Steps: 1. Log in as the test user. 2. Open the page. 3. Enter a single quote.
4. The page shows a database error, confirming the input reaches SQL.
Evidence: Screenshot 4, request and response in appendix B (credentials redacted).
Impact: An attacker could read or change any data in the database.
Fix: Use parameterised queries for every database call; give the app's
database user only the permissions it needs.
References: OWASP SQL Injection Prevention Cheat SheetWrite for two readers. Managers read the executive summary: a short, non-technical page with the overall picture, the most serious issues in business terms and the top priorities. Developers read the detailed findings, so steps must be exact and reproducible and the fix must be specific. A typical report has: title page and version, executive summary, scope and methodology (dates, targets, tools, limitations), a summary table of findings, detailed findings, and appendices.
The summary table can be generated from your findings list:
findings = [
("Missing security headers", 2.3),
("SQL injection in product search", 9.3),
("Other users' orders visible by changing the ID", 7.1),
("Verbose error messages", 5.3),
("Session cookie without Secure flag", 2.1),
]
def rating(s):
return "Critical" if s >= 9 else "High" if s >= 7 else "Medium" if s >= 4 else "Low"
counts = {}
for n, (title, score) in enumerate(sorted(findings, key=lambda f: f[1], reverse=True), start=1):
r = rating(score)
counts[r] = counts.get(r, 0) + 1
print(f"F{n:02} {score:>4} {r:<8} {title}")
print("Totals:", ", ".join(f"{k} {v}" for k, v in counts.items()))F01 9.3 Critical SQL injection in product search F02 7.1 High Other users' orders visible by changing the ID F03 5.3 Medium Verbose error messages F04 2.3 Low Missing security headers F05 2.1 Low Session cookie without Secure flag Totals: Critical 1, High 1, Medium 1, Low 2
Common mistakes
- Scoring by feel. Pick each metric deliberately and include the vector so others can check it.
- Mixing v3.1 and v4.0 scores in one table without saying which is which.
- Steps nobody can repeat. Include the exact page, parameter, account role and expected result.
- Leaving secrets in evidence. Redact passwords, tokens and personal data in screenshots and logs.
- Blaming people. Describe the system's weakness, not the developer who wrote it.
Key points
- CVSS gives a 0.0 to 10.0 severity score and a vector string; v4.0 (November 2023) and v3.1 are both in use.
- Ratings: Low 0.1 to 3.9, Medium 4.0 to 6.9, High 7.0 to 8.9, Critical 9.0 to 10.0.
- v4.0 base metrics are AV, AC, AT, PR, UI and impacts on the vulnerable and subsequent systems.
- Severity is not risk: add business context, CVE, CWE, EPSS and KEV information.
- A good finding has a title, severity and vector, asset, steps, evidence, impact and a specific fix.
Exercise
Use the explain function to describe this vector, then decide which kind of vulnerability it could be: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N. Add a sixth finding, "Stored XSS in product reviews" with score 5.1, to the summary table script and run it.
Show solution
findings = [
("Missing security headers", 2.3),
("SQL injection in product search", 9.3),
("Other users' orders visible by changing the ID", 7.1),
("Verbose error messages", 5.3),
("Session cookie without Secure flag", 2.1),
("Stored XSS in product reviews", 5.1),
]
for title, score in sorted(findings, key=lambda f: f[1], reverse=True):
print(score, title)The vector has no impact on the vulnerable system itself but low confidentiality and integrity impact on a subsequent system after passive user interaction (a victim viewing a page). That pattern fits cross-site scripting, where the vulnerable server is unharmed but the victim's browser session is affected. The new finding is also Medium, so it sorts just after "Verbose error messages" (5.3) and before the Low findings.
Quick Quiz
1. What rating does a CVSS score of 7.5 have?
High covers 7.0 to 8.9.
2. In CVSS v4.0, what does PR:N mean?
PR is Privileges Required; N means None, so an unauthenticated attacker can exploit it.
3. Which source tells you that attackers are actively exploiting a CVE right now?
KEV lists CVEs with evidence of active exploitation, which raises their priority.
4. Who is the executive summary of a report mainly written for?
The executive summary explains the overall picture and priorities in business terms.
Learn Cyber Security with a mentor
Free lessons get you started. Our Ethical Hacking & Cyber Security training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
