Writing findings
A finding only earns its place in the report when an engineer can reproduce it, sees the real impact, and knows the fix. Repro + impact + severity + remediation is the unit of work — a bare severity label is noise.
Two reports land on the same engineering lead’s desk after an authorized engagement. The first says: “High — IDOR on invoices endpoint. Recommend fixing access control.” The lead reads it, can’t reproduce it, doesn’t know which records leak or whether it’s exploitable by an unauthenticated user, and files it under “the pentesters always say this.” Six weeks later the bug is still open. The second report, on the same bug, gives the exact request to replay with two test accounts, shows account B pulling account A’s invoice with name and card last-four, rates it High with a one-line justification, and hands over the three-line server-side fix. It’s closed in a day. Same vulnerability, same severity. The only difference is that one was written as a finding and the other was written as an opinion.
By the end of this lesson you’ll know the anatomy of a finding an engineer can actually act on — reproduction, impact, severity, and remediation — and why the bare severity label that juniors lead with is the least useful part.
The finding is the only deliverable that matters
Everything earlier in this unit — authorization, the toolkit, the methodology — exists to produce one artifact: a finding the customer can fix. A penetration test isn’t graded on how many bugs you found; it’s graded on how many got closed, and a bug only closes when an engineer who wasn’t on the engagement can reproduce it, understand why it matters, and apply a fix. That shifts the whole frame: your audience is not another hacker who’ll be impressed by the exploit, it’s a busy backend engineer with forty other tickets who will deprioritize anything they can’t immediately act on. A finding written for the wrong audience is a finding that stays open — which, for the business, is identical to a vulnerability you never found.
So a finding is a unit of work, not a brag. The senior discipline is to write every finding so that a stranger can pick it up cold and drive it to closure without asking you a single question.
The four parts every finding needs
A finding that closes carries four things, and dropping any one of them is the difference between the two reports in the Hook.
| Part | Question it answers | What “good” looks like | Failure if missing |
|---|---|---|---|
| Reproduction | How do I see it happen myself? | Exact request, accounts, preconditions, expected vs actual | ”Can’t repro” → closed as won’t-fix |
| Impact | What can an attacker actually do? | Concrete: who, what data/action, what damage in this app | Looks theoretical → deprioritized |
| Severity | How urgent is this vs the others? | A rating with a one-line justification you can defend | Inconsistent triage; “everything’s High” |
| Remediation | What exactly do I change? | Root-cause fix, not “sanitize input”; the code-level change | Engineer guesses → wrong or shallow fix |
Reproduction is the part juniors skip and seniors lead with, because it’s the part that converts a claim into a fact. The engineer should be able to follow your steps and watch the bug happen on their own machine: the exact endpoint, the two test accounts and which is which, any preconditions (logged in as a low-privilege user, a specific record id), the precise request you sent, and — critically — the expected vs actual result so the violation is unambiguous. “Account B requested account A’s invoice and received it (expected: 403)” leaves nothing to interpret. Without this, the first thing the customer does is fail to reproduce, and a finding nobody can reproduce gets closed as won’t-fix no matter how real it is.
Impact answers so what. Severity is a number; impact is the sentence that earns the number. The trap is writing the generic impact (“IDOR can lead to unauthorized data access”) instead of the concrete one for this app (“any authenticated user can read every other user’s invoice — name, billing address, and card last-four — by incrementing one integer”). Concrete impact is what stops an engineer from filing the bug under theoretical-pentester-noise; it ties the vulnerability to data and damage they recognize.
Severity is a prioritization signal, and its job is consistency. Use a defined scale — CVSS for catalogued CVEs, a qualitative likelihood × impact rating for logic bugs — and attach a one-line justification you’d defend in the readout. The most common writing failure here is severity inflation: marking everything High to make the report look scary. It backfires — when every finding is High, the customer can’t tell the metadata-credential SSRF from the missing security header, and your real critical drowns. Calibrated severity is what makes the report usable as a work queue.
Remediation has to fix the root cause, not the symptom. “Sanitize the input” is the canonical bad fix — it’s advice, not a change, and it often patches the wrong layer. The good remediation names the actual code-level change: for an IDOR, scope the query to the caller (WHERE id = ? AND owner_id = ?, enforced server-side), not “add validation.” A remediation an engineer can paste-and-adapt is worth ten paragraphs of background.
Writing for the engineer, not the hacker
The hardest shift for a strong tester is realizing the report is a handoff document, and the recipient doesn’t share your context. Three habits separate a finding that closes from one that festers. First, write expected vs actual for every reproduction step — it’s the single highest-leverage sentence, because it makes the violation undeniable and self-checking. Second, never bury the lede: lead with the one-line summary (what + impact + severity), then the detail, because the engineer triaging fifty findings reads the first line of each and budgets from there. Third, make remediation copy-able — point at the exact handler and the exact change, and where you can, link the relevant control (e.g. the WSTG test that maps to it) so the fix is verifiable.
▸Why this works
Why is reproduction the part that actually gets a bug fixed, more than impact or severity? Because it’s the only part the engineer can verify independently before spending effort. Impact and severity are your assessment — the engineer has to trust you. Reproduction is a fact they can reproduce on their own machine in two minutes, and the moment they watch account B pull account A’s invoice, the debate is over: it’s no longer “the pentesters claim X,” it’s “I just saw it happen.” A report with airtight repro and a mediocre severity rating still closes; a report with a perfect CVSS score and no repro gets argued into oblivion. Reproduction is where your credibility is cashed.
A worked finding vs a bare label
Take the Hook’s IDOR. The bad version is one line: “High — IDOR on invoices.” The finding version reads: Summary — any authenticated user can read any other user’s invoice by changing the id (High). Reproduction — log in as test account B, send GET /api/invoices/{A_invoice_id} with B’s session, observe a 200 returning account A’s invoice (name, billing address, card last-four); expected 403. Impact — full read access to every customer’s PII and partial card data across the tenant, exploitable by any logged-in user with no special privilege. Severity — High: trivial to exploit (URL-editable integer), high-value data, broad blast radius. Remediation — enforce ownership server-side on the actual record: change the query to WHERE id = ? AND owner_id = ? (or an equivalent authorization check) on this and every object-by-id handler; deny by default. One of these closes in a day; the other is an opinion.
You found a real IDOR on an authorized engagement. The report is due. Which way of writing it up gives the customer the best chance of actually fixing it?
A customer replies to your report: 'We tried this one and couldn't reproduce it, so we're closing it as won't-fix.' What does this most likely indicate about the finding?
A junior marks every finding in the report 'High' so the client takes the engagement seriously. Why is this a writing failure?
Order the parts of a written finding the way a triaging engineer reads and uses them, from first glance to closing the ticket:
- 1 One-line summary: bug + concrete impact + severity (the lede they triage on)
- 2 Reproduction: exact request, accounts, preconditions, expected vs actual
- 3 Impact: what an attacker can actually do to this app's data/users
- 4 Severity justification: why this rating, defensible in the readout
- 5 Remediation: the root-cause, code-level change that closes it
- 01What are the four parts every actionable finding needs, what does each answer, and what fails if you drop it?
- 02Why is reproduction the part that actually gets a bug fixed, and what does that imply about how you write a report?
The only deliverable of a penetration test that matters is a finding the customer can fix, so a finding is a unit of work, not a brag — and it’s written for the busy engineer who’ll close it, not the hacker who’d admire the exploit. Every actionable finding carries four parts: reproduction (the exact request, accounts, preconditions, and expected-vs-actual that let the engineer confirm the bug on their own machine), concrete impact (what an attacker can really do to this app’s data and users, not the generic textbook line), a calibrated severity with a one-line justification (CVSS for catalogued CVEs, qualitative likelihood × impact for logic bugs — and never inflate everything to High, which destroys the report as a work queue), and a root-cause remediation an engineer can paste and adapt (scope the query to the owner, not “sanitize input”). Reproduction is the part that gets bugs fixed because it’s the only one the customer can verify before spending effort; the moment they watch account B read account A’s invoice, the argument is over. So lead with the lede, write expected-vs-actual for every step, and make the fix copy-able. The bare severity label a junior leads with — “High, IDOR, fix access control” — is the least useful sentence in the report; the finding around it is what closes the bug.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.