---
name: write-finding-report
description: Write a web finding's submission report for a triager who has only the report - its file and style, the description of the bug, its impact, the automated reproduction with the PoC's output, and detailed manual reproduction steps (browser steps for accounts and sessions, every exploit request and its response in full, screenshots only for auth bypass, admin access or purchase proof). Use whenever you write or edit a submission report for a web finding.
---

# Write the finding report

The triager has never seen the application and receives only the submitted report and its attachments. They have no access to our database, our proxy history, or any exchange id. If something is not in the report, they do not have it.

The submission format appended to your instructions decides the sections and their order: use only its sections, in its order, and put in each only what it asks for. This skill decides what goes in them. Where the format limits length (Apple) or excludes manual steps (bugbounty.ch), the format wins. When a `validate-*` skill covers the finding's class, it sets the reproduction steps where it differs from this, so read it before writing.

Write the report as `work/scratch/<finding-id>/report.md`, beside the PoC the `write-finding-poc` skill describes, unless the format names another file. The title lives only in the finding: the body begins with the format's first section.

The reproduction section always has two parts, in this order:

1. **Automated reproduction, required in every report:** the attached PoC's `uv run` command, the same command through a proxy, and the PoC's real output from a clean run.
2. **Manual reproduction:** numbered steps, so a triager can reproduce the bug by hand without the PoC.

The manual steps are added to the automated reproduction, never written instead of it, and never depend on the PoC.

## Writing

- Plain British English, full simple sentences, short ordinary words. No em dashes, no emojis, no marketing.
- No fragment labels. Not "The deprecated build. SAUDIA Android 4.5.9, from <url>." but "Use SAUDIA Android 4.5.9 from <url>."
- No amplification. State each fact once. "The token is a real, privileged application credential for the production booking platform" is "The token is a privileged production credential."
- Never fabricate. Write only what your traffic and evidence show. If you cannot verify a fact, such as where a value came from or whose record it is, leave it out or say it is unknown.
- Copy hosts, paths, parameters, ids, hashes, tokens and versions exactly.
- State the request or action, the target's response or state change, and the consequence, most important first. Words such as "crucially" add nothing, and neither does a sentence that only labels an observation as proof ("This proves", "This confirms", "This demonstrates").
- The report is about the bug, not the testing. Outside the reproduction and a redaction note, leave out what the PoC or script does, the order things were validated in, where testing stopped, what was not accessed, cleanup, and anything done after the impact was established.
- Name commands and payloads and their results without calling them harmless, benign, safe, controlled, bounded or non-destructive, and without forensic qualifiers such as "the directly observed impact" or "not tested or claimed". When a limit matters, say it in ordinary words: "Broader API access was not tested."
- Translate our evidence into requests, responses, URLs, roles and observed behaviour. Never show local paths, table names, row, finding or exchange ids, or evidence links.
- Leave out dates and times unless they correlate asynchronous evidence, tell two states apart or show an expiry. Never state the testing window or repeat a timestamp already in a raw request or the PoC's output.
- Never mention other findings or reports. When this finding relies on another issue, describe that step here as part of this report's flow.
- Give remediation or fix advice only in a section the format has for it.

## Description of the bug

This is the format's first explanatory section: `Detail`, `Summary`, or `Bug description`.

- Open with two to four sentences: the host and endpoint, the parameter, header, field or file involved, the type of bug in plain words, and the headline impact. A triager should understand the bug before they reproduce it.
- Then give the context a triager needs: what the application does with the input, which check is missing or broken, and which boundary it crosses (who can do what they should not).
- Name the roles: unauthenticated, any self-registered user, or a specific role. Public self-registration counts as unauthenticated access.
- Explain any non-obvious part of a payload: why it survives the filter, or what it calls.
- For identifiable third-party software, establish the deployed version from the affected host's own responses where possible and check primary sources for a matching advisory or fix. When the root cause matches one, name the product and observed version, the CVE or GHSA, and the affected and fixed versions, link the primary advisory or fix, and explain any material difference in route, prerequisites, primitive or impact. A matching product and version alone is not a match.
- This is where the bug is explained, so the steps never have to.

## Impact

- Lead with the strongest impact you actually proved, in concrete terms: whose data, which fields, which actions, which privileges. Prefer named data and operations over words like "primitive", "foothold" or lists of hypothetical consequences.
- Demonstrate, do not claim. Every impact sentence is backed by a step in the reproduction.
- Show the data, never a count. "Returns 4,812 users" proves nothing; one real record with its sensitive fields does.
- Cross-user impact uses two accounts you control. Show account A reaching account B's data or actions, and the field that proves it is not A's.
- Scale is shown, not asserted. Say where an attacker gets valid ids (a listing, a predictable sequence, a public page) and that the resource is private by default.
- Give one realistic abuse scenario when the format asks for one: who the attacker is, what they need, and what they end up with.
- Hedge only with a reason. "Accounts here need manual approval, so a second-account test was not possible" is fine. A hedge with no reason is not.
- Never inflate. The impact matches what the steps show.

What to show by class:

- **SQL injection:** a value read from the database, such as the database user, the version, or one row. For blind injection, show the response time tracking the injected sleep value (two requests with different values and their timings), or a true and a false condition giving different results.
- **Command execution:** the default command of the `validate-command-execution` skill (`whoami; hostname; uname -a`), with its real output. Never environment variables, credentials or other files.
- **SSRF:** the response from the internal or metadata target. Reaching cloud credentials shows the impact; never use them.
- **File read:** the most sensitive file your evidence reached, such as a config with secrets, shown in full unless it is huge.
- **Access control or IDOR:** the victim's record with the field that proves it is the victim's.
- **Authentication bypass, session theft, account takeover:** one or two requests made with the obtained session that return the account's most sensitive data, and the post-bypass page when it can be seen in a browser (see Screenshots).
- **XSS:** a payload of `alert(document.domain)`, the request that stores or reflects it, and the response serving it unescaped in an executable context. For stored XSS, show it served to a second account.
- **Leaked or hardcoded secret:** the secret used once against the live API it belongs to, in a read-only call that returns real data, such as the identity it authenticates as. A secret only listed is informational.
- **Write, create or delete:** shown on an object you created: the request that changes it and the read-back that shows the change. Never only describe a write you could have shown on your own object.

## Automated reproduction

The reproduction section opens with the PoC run, in the structure the format gives:

- any unavoidable manual prerequisite;
- the canonical non-proxy `uv run <name>.py <arguments>` command in a `bash` or `sh` block, which is the command run in isolation and, when a video is required, recorded;
- the equivalent `--proxy` command;
- the PoC's stdout from a clean final run, verbatim. Redact only as Redaction below allows, and never replace the output with a rewritten success summary.

## Manual reproduction steps

The steps are the report.

### Shape

- One numbered block, numbered continuously. Write the steps as `1.`, `2.`, and indent every code block and image three spaces under its step, so the list never restarts. Never skip, repeat or restart numbers.
- No `Prerequisites:` or `Setup:` block. Step 1 is the first thing the triager does. Fold each prerequisite into the step where it is first needed: the signup page, the region, the browser, the button name.
- When the bug needs a victim and an attacker, split the steps under `### As the Victim User` and `### As the Attacker`, keeping one continuous count across both.
- Only when the attacker needs a value they cannot predict (a victim UUID, an internal object id), add one final subsection, `#### How to obtain UUID / Unpredictable values`, saying how to get each one. Leave it out otherwise.

### Steps are directions and observations

- Each step is one action as a plain instruction: one request, one change to a request, one click, one switch between accounts, or one observation. Never join two actions with "and" or "then"; split them.
- A single logical action stays one step. "Register an account at <url>" is one step even though the form has many fields.
- Follow a step that can be missed with one line of what the triager will see ("The response contains `"role":"admin"`").
- Never explain why the bug works in a step. Delete any clause like "the important property is", "this works because", "note that", "any value works", "is attacker-controlled", "as you can see", or "which proves". The description explains the bug; the steps only reproduce it.
- No negative-control steps, such as showing that no auth gives a 401 or a wrong key is rejected. An injection's own baseline and modified request are not controls; they are the exploit.
- Refer back to an earlier step only for a value ("the order id from step 6"), never for a request.

### Accounts and sessions: browser steps

Get accounts and sessions through the browser, not by pasting the requests of a login flow. Write each as its own numbered step:

```
1. Register an account at https://app.example.com/signup.
2. Open the confirmation link sent to your email.
3. Set your browser to proxy through Burp Suite or Caido, with its CA certificate trusted.
4. Log in at https://app.example.com/login.
5. In the proxy HTTP history, open the request to `GET /api/v2/me` and copy the `session` cookie. This is ATTACKER_SESSION in the steps below.
```

- Give the exact URLs, the button and menu names in quotes ("Invite", "Post Job Without Budget"), and the one small action that blocks reproduction if missed, such as confirming the email first.
- For a second account, have the triager register it in a separate browser profile or private window, and name its value (VICTIM_SESSION).
- Name exactly what to copy and from where: the cookie name, or the `Authorization` header, from a named request in the proxy history.
- Write browser steps only for pages and controls you used. Walk them yourself in `agent-browser` while validating, and never invent a browser flow from API traffic.
- Paste a login or token request instead only when there is no browser flow (an API-only target), or when one clean request issues the credential, such as an API key endpoint.
- For a key hardcoded in a binary or bundle, step 1 is the download URL and the literal commands that extract it (`curl -O`, `apktool d`, `grep -ri`), with the output line showing the key. The next steps use that key against the live API.

### Requests and responses

- Every step that sends a request shows the full raw request in its own fenced `http` block: method, path and query, `Host`, the cookies, `Authorization` and other headers the app needs, the content type, and the body.
- Strip testing-only headers (`X-Bug-Bounty`, the engagement's identifying header, `X-Pentest`) and browser noise (`Sec-*`, `Referer`, unrelated tracking cookies). Everything else stays exactly as sent.
- Never deduplicate. Never write "repeat the request from step N", "same as step N", "as above", or show only the changed lines. Each request is pasted complete in the step that sends it, even when it is almost identical to an earlier one.
- For an injection, show the full baseline request, then the full modified request in its own block, with one sentence naming the change ("the only change from step 7 is `"format":"plain"` to `"format":"html"`").
- Carried values: give each one a name in capitals in the step text (VICTIM_ORDER_ID), and use the real value you captured in the request block. Tell the triager which values to swap for their own ("use your ATTACKER_SESSION from step 5 in the `Cookie` header").
- When the response is the proof, or carries a value a later step uses, paste it as text in an `http` block under the request: the status line, the headers that matter, and the relevant part of the body. Trim only genuinely irrelevant parts of a long body.
- After a response that proves something, add one plain sentence naming the value ("`JCY01PA11` in the response is the database user"), not "This proves the injection".
- A live session will expire before triage. Show the real value and the steps that refresh it, never one instead of the other.

### Values

- Every value the triager cannot invent says where it came from: an order or account id, a victim user id, a booking reference, a token, a file path, an email. Either say how the triager gets their own (the request, page or listing that yields one), or where this worked example came from (your own account, a public page, an earlier step).
- Search the finding's evidence and traffic (`toadcli traffic`, `toadcli search body`) for where a value first appeared. If its origin is not recorded, say the triager can supply any valid value and how to find one. Never invent a provenance, and never claim a record is yours unless you created it.
- Computed values: a value that did not come straight from a response (a signature, an encoding, a forged token, a value pulled from a binary) is produced by a command or short script shown in the step, with its real input and output, taking only user-specific inputs as arguments. Never hand the triager a template to fill in by hand ("build the assertion with the current UTC time"). If it needs more than a short snippet, attach a small generator script and have one step run it.

### Screenshots

Use a screenshot only where the proof is visual in the application:

- An authentication bypass: the page reached after the bypass, such as the victim's account page.
- Access to an admin panel or another privileged interface: the panel as the unprivileged or unauthenticated user sees it.
- Proof of purchase or payment state: the order confirmation, receipt or checkout showing the manipulated price, the free item or the unpaid order.

Capture it in the browser session the steps describe, and prepare it with the `document-finding-screenshots` skill. Place it inline right after the step it backs, and publish it with `--screenshot`. Everything the triager needs to replay (the request, the payload, the leaked value) is also in the text; a fact that lives only in an image cannot be replayed. Never screenshot proxy history or terminals for HTTP proof, and never render your own HTML as a screenshot.

### Redaction

Show what belongs to your own test accounts exactly as sent and received: their cookies, `Authorization` headers, tokens, API keys and parameters. Never mask them, star them out, or replace them with `<your session>` or `YOUR_TOKEN`; the triage team needs the real values to replay the requests.

Redact what the bug leaked and is not yours: the target's credentials, keys, tokens and signing material, another user's session or password, and other people's personal data. This holds in the prose, the request and response blocks and the copied PoC output. Keep enough of the value to show its shape and that it is real (the first and last four characters of a key, `j***@example.com`), put `[redacted]` in place of the rest, and say where the triager gets the whole value, which is by running the PoC or replaying the request. The engagement's rules may ask for more redaction than this, never less.

## Replay before publishing

Walk the manual steps live from a clean start exactly as written: register or log in through the browser steps, copy the values the steps name, and send each request block as shown. Fix any step that does not give the stated result. Steps you did not replay are not finished.

## Final check

The report is ready only when all of these are true:

1. The description names the host, endpoint, parameter and bug type in plain words, and explains the bug so the steps do not have to.
2. The impact is the strongest proven, shown with real data rather than counts, matched to the steps, and not inflated. Any untested part says why.
3. The reproduction section opens with the automated reproduction (the PoC's `uv run` command, its proxy variant and its real output), and the manual steps follow it.
4. A triager with no knowledge of the application can reproduce the bug from the manual steps alone, starting at step 1 with no prior setup.
5. Accounts and sessions come from browser steps that name the URLs and exactly what to copy from the proxy history, unless there is no browser flow.
6. Every step is a single action, with no explanation of why the bug works and no negative controls.
7. Every step that sends a request shows the complete request in its own block, with no back-references, and every proving response is pasted as text.
8. The numbering is continuous and survives code blocks and images; a two-actor bug is split into the two role sections.
9. Every value has a stated origin, and every computed value comes with the command or script that produced it.
10. Screenshots appear only for an auth bypass, admin access or purchase proof, inline after their step.
11. Your own test accounts' values are shown whole, and every leaked secret and other people's personal data is redacted and labelled as Redaction says.
12. You replayed the manual steps live and they gave the stated results.
