Skip to content
TRIAGERS™
Deep Dives · 9 min read · TRIAGERS™ Team

How to Triage Bug Bounty Reports: A Step-by-Step Walkthrough

A practitioner's guide to triaging a bug bounty report properly: reproduction discipline, dedup, severity, engineering handoff, and a worked IDOR example.

Most triage failures happen in the first ten minutes, when the triager skips straight to "can I reproduce this?" without first working out what the researcher is actually claiming. Everything downstream (the dedup check, the severity call, the engineering handoff) inherits the sloppiness of that first read.

This post walks through how we triage a single report, end to end. It assumes you already know what bug bounty triage is and why it exists. This is the operational layer: what a competent triager actually does between "new report arrives" and "engineering has a ticket they can act on."

Step 1: Read for the claimed impact, not the title

Report titles lie. Sometimes deliberately (severity inflation is a sport), usually just through imprecision. "Account takeover via password reset" might describe a genuine ATO chain, or it might describe a self-XSS that requires the victim to paste JavaScript into their own console while standing on one leg.

Before touching a terminal, answer three questions from the report text alone:

  1. What does the researcher claim an attacker can do? Not the vulnerability class, the outcome. "Read another user's invoices" is an impact. "IDOR on /api/invoices" is a mechanism.
  2. What does the attacker need? An account? A paid plan? A victim to click something? Physical access to the victim's laptop? Prerequisites are where inflated reports quietly fall apart.
  3. Is the claimed impact actually demonstrated, or asserted? A screenshot of a 200 response is not the same as a screenshot of someone else's data.

Write these three answers down. They become the skeleton of your internal summary later, and they keep you honest when you're two hours into a reproduction rabbit hole wondering what you were trying to prove.

Step 2: Check for duplicates before you burn an hour

Dedup comes before reproduction, not after. Reproducing a bug you already have in the tracker is a waste of the scarcest resource a program has: triager attention.

Search your report history by endpoint, by parameter, and by root cause, in that order of increasing usefulness. Two reports on different endpoints can share a root cause (one missing authorization check in a shared middleware), and marking the second one "new" means engineering gets two tickets for one fix. We've written a full post on why naive dedup fails, but the short version: dedup on root cause, not on URL string similarity.

If it's a probable dupe, note the original report ID and keep going: you still need enough reproduction to confirm it's actually the same issue, not a neighbouring one.

Step 3: Set up the environment deliberately

The most common self-inflicted triage wound is testing in the wrong context. Before reproducing:

  • Use accounts you control for both roles. For any authorization bug you need two accounts minimum: attacker and victim. Populate the victim account with identifiable data so you can prove cross-account access unambiguously.
  • Match the researcher's context. Free tier vs paid tier, org member vs org admin, verified vs unverified email. A surprising number of "can't reproduce" verdicts are really "reproduced in the wrong tier."
  • Capture everything. Proxy on, logging on, from the first request. You will want the exact request/response pairs for the engineering ticket, and re-reproducing just to get screenshots is misery.

If the program has a staging environment, prefer it, but confirm the bug exists in production before closing as informative, because staging and production drift.

Step 4: Reproduce with discipline

Follow the researcher's steps exactly first, even the ones that look redundant. Only after you've seen the bug once do you start simplifying: removing steps, minimising the request, isolating the vulnerable parameter. The goal is a minimal reproduction: the shortest sequence of requests that demonstrates the impact.

Two rules that save careers:

  • Never test destructive impact on data you don't own. If the report claims you can delete another user's account, prove the authorization flaw with a read or a harmless write on your own second account. Don't go deleting things to see what happens.
  • Distinguish "cannot reproduce" from "did not reproduce." The first is a verdict; the second is a status. Before you tell a researcher their bug doesn't exist, exhaust the obvious variables: account state, region, feature flags, caching, and whether the vendor shipped a fix between report and triage. Check the deploy log if you can.

Step 5: Asking the researcher for more info (without burning goodwill)

Sometimes you genuinely need more from the researcher. How you ask matters, because researchers talk to each other and a program with a reputation for lazy clarifying questions gets worse reports and fewer of them.

The rule: demonstrate effort before requesting it. Compare:

"Please provide more details and a video PoC."

with:

"I've reproduced steps 1–3 and I can see the invoice ID in the response. At step 4, my request to PATCH /api/invoices/1337 returns 403 rather than the 200 in your screenshot. I'm testing with a free-tier attacker account. Were you on a paid plan, or is there a header I'm missing from your request?"

The second version gets a fast, precise answer. It shows you did the work, it narrows the question to one variable, and it treats the researcher as a collaborator rather than a suspect. It also protects you: if the researcher can't answer a question that specific, that itself is signal, increasingly relevant in the era of AI-generated slop reports.

Never use a clarifying question to stall an SLA clock. Researchers notice, platforms notice, and your triage metrics will look great right up until your program's reputation doesn't.

Step 6: Write the internal summary

The internal summary is for two audiences who will not read the original report: the engineer who fixes it and the manager who prioritises it. Ours look roughly like this:

## Summary
Authenticated user can read any other user's invoices by
incrementing the numeric ID in GET /api/invoices/{id}.
Authorization check is missing on the read path (write path
correctly returns 403).

## Impact
Full invoice data disclosure across all tenants: names,
addresses, line items, partial payment details. ~sequential
IDs make enumeration trivial.

## Reproduction (minimal)
1. Authenticate as any user, capture session token.
2. GET /api/invoices/2041 (an ID belonging to another tenant)
3. Observe 200 with full invoice JSON.

## Affected component
invoice-service, InvoiceController#show: no tenant scoping
on the query.

## Suggested fix
Scope invoice lookup to current_user's tenant; consider
non-sequential IDs as defence in depth (not as the fix).

Notice what's not in there: the researcher's speculation, the fifteen screenshots, the CVSS debate. Engineers need mechanism, impact, repro, location, fix direction. Everything else is noise.

Step 7: Propose severity

Severity is an argument about impact under realistic conditions, not an arithmetic exercise. Use CVSS or the Bugcrowd VRT as a shared vocabulary (they're useful for consistency), but apply judgement on top:

  • Adjust for prerequisites. An IDOR requiring an org-admin attacker in the victim's own org is a different animal from one any free account can exploit.
  • Adjust for data sensitivity. "Read other users' data" spans everything from public display names to medical records. The class doesn't set the severity; the data does.
  • Write the reasoning down. One or two sentences: "High rather than Critical because exploitation requires a paid account and exposed data excludes credentials/payment instruments." When the researcher disputes it (and they will), you respond with reasoning, not a rating.

Step 8: Handoff to engineering

A triaged report that sits in a queue is just a well-documented liability. The handoff needs three things: the internal summary above, a severity with reasoning, and a suggested owner: the team or service most likely responsible. A triager who can read a stack trace and name the probable service cuts days off time-to-fix.

Then update the researcher: confirmed, severity proposed, handed to engineering. Silence after triage is the second-biggest cause of researcher rage, right after dupe disputes.

A worked example: the IDOR report

Here's the whole flow compressed into one realistic case. A report arrives:

Title: Critical IDOR: full account takeover Body: The endpoint GET /api/v2/users/{id}/export returns the account export of any user. Change the ID and you get their data. This is a critical account takeover. PoC: curl -H "Authorization: Bearer <token>" https://app.example.com/api/v2/users/8812/export

Read for claimed impact. The claim is cross-user data disclosure via an export endpoint. Note: "account takeover" is asserted, not demonstrated: nothing here takes over anything. Prerequisite: any authenticated user.

Dedup. Search history for /export, for the users controller, and for prior missing-authz reports in the same service. We find an old report on PUT /api/v2/users/{id}/settings: same controller, but that was a write-path bug fixed eight months ago. Different check, plausibly same pattern, but not a dupe. Worth mentioning in the ticket.

Environment. Two fresh accounts. Victim account (user ID 9107) gets a distinctive display name and a saved payment method (our own test card) so disclosure is provable.

Reproduce. As the attacker:

GET /api/v2/users/9107/export HTTP/1.1
Host: app.example.com
Authorization: Bearer eyJhbGciOi...attacker-token

Response: 200 OK, full JSON export of the victim account: profile, email, address, last-4 of the card, and a password_hash field someone helpfully left in the serializer. That last detail wasn't in the report and it changes the severity conversation.

Severity. The researcher said Critical/ATO. It isn't ATO: no authentication material that directly grants access. But bcrypt hashes plus full PII across all users via trivial enumeration is a strong High, arguably Critical depending on the program's policy on offline-crackable material. We propose High, write two sentences of reasoning, and note the hash exposure as an aggravating factor for engineering to kill regardless.

Respond and hand off. The researcher gets: confirmed, High, note that ATO wasn't demonstrated but the hash exposure was a good find beyond their own claim (credit generously; it's cheap and it's honest). Engineering gets the minimal repro, the serializer as the probable fix site, and a flag that the export feature likely needs an audit for other over-serialized fields.

Total time for a triager who's done this a few thousand times: 45–90 minutes. Total time for an engineer pulled off feature work to do the same: usually most of a day, done worse. That gap is essentially the entire economic case for specialist triage.


TRIAGERS™ provides on-demand triage teams that do exactly this, at whatever volume your program throws at them, self-hosted or on HackerOne, Bugcrowd, Intigriti, or YesWeHack. If your engineers are the ones doing the 45–90 minutes above, get in touch.

Keep reading
The Triage Brief

New articles, straight to your inbox.

Practical triage writing, published a few times a month. Unsubscribe anytime.

Or grab the RSS feed.

Drowning in unread reports?

Lease an expert triage team that validates, reproduces and rates every submission, so your engineers only see signal.

Get a triage team