A penetration testing report is the product the client pays for. The testing is how you produce it. A good one tells executives what the risk is in one page, gives developers enough detail to reproduce and fix each issue, and gives auditors a record of what was tested and how.
This guide covers the structure most professional reports follow, how to score findings with CVSS, and how to write a finding that actually gets fixed. It ends with a sample report extract for a fictional company, Acme Fintech, which you can use as a template for your own lab write-ups.
Legal note: only test systems you own or have written permission to test. The sample below describes a fictional company and a fictional lab environment.
Who reads a penetration testing report?
Write for three audiences, often in the same organisation:
| Reader | What they need | Where they look |
|---|---|---|
| Executives, board, risk owners | Overall risk, business impact, what needs a decision or budget | Executive summary only |
| Developers and engineers | Exact affected endpoints, reproduction steps, evidence, how to fix | Detailed findings |
| Auditors, regulators, customers' due-diligence teams | Scope, dates, methodology, independence, retest status | Scope and methodology, appendices |
If you write only for the technical reader, the report stalls at management level. If you write only for executives, developers can’t act on it. The structure below serves all three.
Penetration testing report structure (a template you can reuse)
Most professional reports follow roughly this order:
- Cover and document control
- Client, report title, version, date, classification (for example “Confidential”)
- Distribution list, author and reviewer roles
- Executive summary
- One page in plain language: what was tested, overall risk, the few findings that matter most, and what to do next
- Scope and methodology
- In-scope and out-of-scope targets, testing window and source IPs
- Test accounts used, approach (black, grey or white box), and the methodology followed
- For web apps, many testers reference the OWASP Web Security Testing Guide; for broader engagements, NIST SP 800-115
- Findings summary
- One table with ID, title, severity, CVSS score and status
- Detailed findings
- One section per finding (template below)
- Positive observations
- Controls that held up under testing. Clients need to know what to keep.
- Appendices
- Tools and versions, full evidence, scan outputs, retest results, glossary
Deliver it as a PDF, marked with its classification, and send it through an agreed secure channel. A pentest report is effectively a map of how to attack the client, so treat it that way. Redact any secrets you captured, such as passwords, tokens and personal data. Show enough to prove the point, and no more.
How to score findings: CVSS v3.1 and v4.0
Most reports score technical severity with the Common Vulnerability Scoring System (CVSS), maintained by FIRST. Two versions are in common use:
- CVSS v3.1 is still widely used in reports and vulnerability databases.
- CVSS v4.0 is the newest version. It changes several metrics and adds supplemental ones.
Pick one version per report and say which in the methodology section. The sample below uses v3.1. A v3.1 base score comes from eight metrics written as a vector string:
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
That reads: Attack Vector Network, Attack Complexity Low, Privileges Required Low, User Interaction None, Scope Unchanged, Confidentiality High, Integrity High, Availability None. Always include the vector, not just the number, so the client can see and challenge your reasoning. FIRST’s v3.1 calculator gives the score.
The v3.1 specification maps scores to qualitative ratings:
| Score | Rating |
|---|---|
| 0.0 | None |
| 0.1–3.9 | Low |
| 4.0–6.9 | Medium |
| 7.0–8.9 | High |
| 9.0–10.0 | Critical |
Remember that CVSS measures technical severity, not business risk. A Medium finding on a payment flow may matter more to a fintech than a High on an internal test server. Many testers add a short business-impact note to each finding for exactly this reason.
How to write a single finding
Each finding should stand on its own, because developers often get one finding pasted into a ticket. Use the same template every time:
- ID and title: specific. "IDOR on beneficiary endpoint allows access to other customers' data“, not ”Access control issue".
- Severity, CVSS score and vector
- Affected assets: exact hosts, URLs, endpoints, parameters
- Description: what the flaw is, in two or three sentences a non-specialist can follow
- Impact: what an attacker could realistically do, tied to the client’s business
- Steps to reproduce: numbered, with raw requests and responses
- Evidence: screenshots or output, redacted
- Remediation: the fix for the root cause, not just the exact payload you used
- References: OWASP, CWE or vendor guidance
- Status: open, fixed, partly fixed, risk accepted (updated at retest)
Keep notes this way during testing, and the report almost writes itself. Our walkthrough of the phases of a penetration test shows where note-taking fits in the engagement.
Sample penetration testing report extract: Acme Fintech
Fictional sample. Acme Fintech, its systems, people and data are invented. The environment is a lab using the
.testdomain and documentation IP ranges.
Executive summary
Acme Fintech engaged us to test its customer web application and mobile API at app.acme-fintech.test and api.acme-fintech.test between 21 and 25 September 2026. Testing was performed from 203.0.113.10 using two customer test accounts and one support-staff test account supplied by Acme Fintech.
Overall, we rate the application’s security posture as moderate, with one high-severity issue that needs urgent attention. A logged-in customer could view and change the saved beneficiaries of other customers by altering an account number in a request. An attacker with any valid customer account could use this to redirect another customer’s future transfers to an account they control. We reported this to Acme Fintech’s security contact on 23 September, the day it was confirmed.
We also found that support staff can export full customer records through an administrative function they should not reach, and that the password reset page reveals whether an email address belongs to a customer. Authentication, session management and transfer-limit controls held up well under testing.
Recommended next steps:
- Fix F-01 and F-02 before the next release.
- Add server-side ownership and role checks across the API, not only on the reported endpoints.
- Arrange a retest of all findings once fixes are deployed.
Findings summary
| ID | Title | Severity | CVSS v3.1 | Status |
|---|---|---|---|---|
| F-01 | IDOR on beneficiary endpoint allows reading and modifying other customers' beneficiaries | High | 8.1 | Open |
| F-02 | Support role can call administrative customer-export function | Medium | 6.5 | Open |
| F-03 | Password reset response reveals registered email addresses | Medium | 5.3 | Open |
| F-04 | HTTP Strict Transport Security (HSTS) header not set | Low | 3.1 | Open |
| F-05 | Default Apache Tomcat page exposed on port 8080 | Informational | n/a | Open |
Informational items are listed in Appendix A.
F-01: IDOR on beneficiary endpoint allows reading and modifying other customers' beneficiaries
Severity: High · CVSS v3.1: 8.1 · CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
Affected: GET and PUT https://api.acme-fintech.test/v1/accounts/{account_id}/beneficiaries
References: OWASP API Security Top 10 2023, API1: Broken Object Level Authorization; CWE-639: Authorization Bypass Through User-Controlled Key
Description. The API returns and updates beneficiary records based on the account_id in the URL. It does not check that the account belongs to the authenticated customer. Any logged-in customer can therefore access another customer’s beneficiaries by changing the number.
Impact. An attacker with a valid customer account can read other customers' saved beneficiaries, including names, bank names and account numbers, and can replace them. A customer who later pays a saved beneficiary without checking the details could send money to the attacker. Account numbers appear to be sequential, which makes targeting straightforward.
Steps to reproduce.
- Log in as test customer A (account 100481) and capture the bearer token.
- Send the following request, replacing A’s account number with test customer B’s (100482):
GET /v1/accounts/100482/beneficiaries HTTP/2
Host: api.acme-fintech.test
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...[redacted, customer A]
HTTP/2 200 OK
Content-Type: application/json
{“account_id”:100482,“beneficiaries”:[{“id”:“b-7731”,“name”:“[redacted]”,“bank”:“[redacted]”,“account_no”:“******4410”}]}
- Send a
PUTto the same path with a modifiedaccount_no. The API responds200 OK, and logging in as customer B shows the changed beneficiary.
Testing was limited to the two supplied test accounts. No other customer data was accessed.
Remediation. On every request, enforce on the server that the authenticated user owns or is authorised for the requested account_id. Apply this in a shared authorisation layer rather than per endpoint. Consider using non-sequential identifiers as defence in depth, but not as the fix. Require step-up authentication, such as a transaction PIN or OTP, before a beneficiary can be changed. Add automated tests that call each object-level endpoint with a second user’s token.
F-02: Support role can call administrative customer-export function
Severity: Medium · CVSS v3.1: 6.5 · CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
Affected: POST https://api.acme-fintech.test/v1/admin/customers/export
References: OWASP API Security Top 10 2023, API5: Broken Function Level Authorization
Description. The admin interface hides the “Export customers” button from support staff, but the API endpoint behind it does not check the caller’s role. A support-staff token returns a full CSV export.
Impact. Any support-staff account, or an attacker who compromises one, can bulk-export customer personal data. That creates a data-protection exposure as well as a security one.
Steps to reproduce. Authenticate as the supplied support test user, then send POST /v1/admin/customers/export with an empty JSON body. The response is 200 OK with Content-Type: text/csv and customer records. Evidence is in Appendix B, with records redacted.
Remediation. Enforce role checks on the server for every administrative endpoint, and deny by default. Log and alert on bulk exports. Review other /admin/ routes for the same pattern.
F-03: Password reset response reveals registered email addresses
Severity: Medium · CVSS v3.1: 5.3 · CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N
The reset form returns “No account found for this email” for unregistered addresses and “Reset link sent” for registered ones. Attackers can use this to confirm which email addresses belong to customers, which helps targeted phishing and password spraying. Fix: return the same generic message in both cases, for example “If an account exists, we’ve sent a reset link”. Rate-limit the endpoint and alert on volume.
F-04: HSTS header not set
Severity: Low · CVSS v3.1: 3.1 · CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
The application redirects HTTP to HTTPS but does not send a Strict-Transport-Security header. A user on a hostile network could be exposed to a downgrade attack on their first visit. Fix: send Strict-Transport-Security: max-age=31536000; includeSubDomains on all HTTPS responses once every subdomain supports HTTPS.
Positive observations
- Session tokens were invalidated on logout and expired after inactivity.
- Transfer limits were enforced on the server; changing amounts in client requests had no effect.
- Login rate limiting and account lockout worked as designed.
Common mistakes in pentest reports
- Pasting scanner output as findings. Verify every issue by hand. Unconfirmed scanner results belong in an appendix, labelled as such.
- Severity inflation. Calling everything High teaches clients to ignore you. Show the vector and justify it.
- Vague remediation. “Implement proper access control” helps no one. Say where and how.
- Missing scope details. If the report doesn’t say what wasn’t tested, readers assume everything was.
- No peer review. A second tester catches wrong CVSS vectors, unclear steps and leftover sensitive data.