Security research
Step 317. Verifying Candidates and Confirming Impact
Level 4 — Bug Bounty | Difficulty ★★★★☆ | Estimated time: 3 hours
Prerequisites: you have finished Step 316 (feature-by-feature discovery). You have
candidate_list.mdin hand.
- What you need: Python 3, Step 316’s extended lab (
app.py),requests, and Step 316’s candidate list. - ⚠️ All exercises in this chapter are for your own lab and legal platforms only. Applying them to unauthorized systems is a crime.
- Caution: the verification process itself must cause no harm — bulk reading of others’ data, service load, and real-user impact are all forbidden. Proving impact with the minimum of requests and stopping — that is a professional’s verification.
From Step 316 you got six candidates. But a candidate is not a vulnerability. "It throws an error" is not a vulnerability; "I can read someone else’s data" is a vulnerability. Today you put the candidates on the judgment stand — is it reproducible, is there security impact, and is that verification harmless. Only what passes all three questions goes on to a report.
1. Learning Objectives
By the end of this chapter, you will be able to:
- Apply the "is it a bug or not" judgment criteria (reproducibility + security impact)
- Sort candidates by impact to decide the verification order
- Perform minimal verification with test accounts A/B and stop immediately
- Save raw requests/responses as evidence and mask sensitive information
- Make the judgment to hold a candidate whose impact proof is blocked and move on
2. Background Knowledge — Today’s Tools and Concepts
Today’s Tools at a Glance
| Category | Details |
|---|---|
| Language/environment | Python 3, Flask mock lab, requests |
| Today’s tools | (hands-on) minimal verification + evidence-saving script verify.py, (concept intro) saving raw requests in Burp |
| Concepts needed | Reproducibility, security impact, PoC minimization, impact-scope estimation, evidence masking |
| Today’s deliverable | 1 confirmed valid vulnerability + evidence_*.txt evidence files |
2-1. The Two Conditions of a Vulnerability
For a candidate to become a real vulnerability, it must satisfy two things at once.
- Reproducible — anyone (including the triager) who follows the same steps gets the same result
- Security impact — what an attacker could do with this exists concretely
Missing either one, the report is rejected. "A weird response comes back" is reproducible, but without impact it’s an informative verdict. Conversely, even if impact seems present, if it can’t be reproduced, it can’t pass review.
2-2. Sorting by Impact — What to Verify First
Verification time is finite, so check the highest-impact candidates first. The general order.
| Rank | Type | Reason |
|---|---|---|
| 1 | Others’ data access (IDOR, etc.) | Immediate info leak with no conditions |
| 2 | Auth bypass / account takeover | Seizes the victim’s whole account |
| 3 | Stored XSS | Executes in other users’ browsers |
| 4 | Reflected XSS, info disclosure, etc. | Requires a victim’s action or has limited impact |
2-3. PoC Minimization — The Boundary of Proof
The rule of a PoC (Proof of Concept) is "the minimum needed to prove." For IDOR, reading 1 record of someone else’s data proves the flaw holds — reading 100 is not proof but collection, and at that point it becomes intrusion. For DoS-class issues, verification itself is often forbidden in the real world, so you stop at "theoretical possibility" and state that fact in the report.
The boundary of verification in one line — "stop the moment you see it holds."
2-4. Collecting and Masking Evidence
Evidence goes with a report — raw requests/responses (Burp’s raw request), screenshots, screen recordings. At that point, the evidence must not immortalize someone else’s personal info or your session cookie as-is. Make a masked copy of session values, addresses, and emails, and handle the raw originals only inside the report channel. The hygiene of evidence is the reporter’s credibility.
3. Follow Along
3-1. Deciding the Verification Order
Sort Step 316’s candidate list by 2-2’s criteria.
1st: T5 IDOR (reading others' personal info) — immediate leak with no conditions
2nd: T6 reset-token prediction (account takeover) — need to confirm the token-use path
3rd: T1 no session invalidation — session theft must happen first
4th: T4 dev /admin exposure — dev environment only, no data
5th: T2 reflected XSS — requires a victim's click
6th: T3 upload bypass — execution unconfirmed
How to read it: today you verify the 1st-priority T5 to the end, and decide the others’ fates along the way. If the lab isn’t up, start it with python app.py.
3-2. Minimal Verification — verify.py
Confirm the IDOR candidate with "exactly 2 requests." One of mine, one of B’s — that’s all.
import requests, re
API = "http://127.0.0.1:5003"
a = requests.Session()
a.post(f"{API}/api/v1/login", json={"user": "alice", "pw": "alice-pass!"})
# minimal verification: exactly 1 each of my resource and B's resource
mine = a.get(f"{API}/api/v1/users/1001")
bob = a.get(f"{API}/api/v1/users/1002")
print("[verify] A session -> A (1001):", mine.status_code)
print("[verify] A session -> B (1002):", bob.status_code)
print("[verdict]", "impact proven — read 1 record of others' data, stopping here"
if bob.status_code == 200 else "impact unproven — hold")
# evidence save: raw request/response + sensitive-info masking
raw_req = bob.request
req_text = (f"{raw_req.method} {raw_req.path_url} HTTP/1.1rn"
f"Host: 127.0.0.1:5003rnCookie: session=<session value masked>rn")
resp_masked = re.sub(r'"email":s*"[^"]+"', '"email": "<masked>"', bob.text)
resp_masked = re.sub(r'"addr":s*"[^"]+"', '"addr": "<masked>"', resp_masked)
with open("evidence_idor.txt", "w", encoding="utf-8") as f:
f.write("== raw request ==n" + req_text +
"n== response (sensitive info masked) ==nHTTP/1.1 200 OKn" + resp_masked + "n")
print("n[evidence] evidence_idor.txt saved:")
print(req_text + "---nHTTP/1.1 200 OKn" + resp_masked)
# scope estimation: check whether IDs are sequential (exactly 1 extra request — for boundary checking)
probe = a.get(f"{API}/api/v1/users/1003")
print("n[scope] nonexistent ID (1003):", probe.status_code,
"— sequential ID structure; exposure estimated at the number of accounts")
Input:
python verify.py
Output (measured 2026-09-09):
[verify] A session -> A (1001): 200
[verify] A session -> B (1002): 200
[verdict] impact proven — read 1 record of others' data, stopping here
[evidence] evidence_idor.txt saved:
GET /api/v1/users/1002 HTTP/1.1
Host: 127.0.0.1:5003
Cookie: session=<session value masked>
---
HTTP/1.1 200 OK
{"addr": "<masked>","email": "<masked>","id":1002,"phone":"010-****-1002","user":"bob"}
[scope] nonexistent ID (1003): 404 — sequential ID structure; exposure estimated at the number of accounts
How to read it: see three stages. ① Verification finished in 2 requests — the moment 1002 was read, the flaw was proven, and you stopped there. ② In the evidence file, the session cookie and the email/address are masked. ③ Scope estimation was done with a "nonexistent ID" — the 404 confirms "the IDs are sequential numbers," and you did not open any other real account’s data. That last bit of restraint is PoC minimization.
3-3. Recording the Scope — Separating the Verified from the Estimated
Write what you just confirmed and what you estimated separately. This distinction decides the next chapter’s (the report’s) credibility.
Verified: with account A's session, account B (id 1002)'s email, phone, and address
can be read.
Estimated: since the IDs are sequential numbers (1001, 1002, ...), exposure likely
extends to the full number of accounts.
(No other real IDs were opened — 1 record of proof is enough)
How to read it: "verified" is a fact with raw request/response evidence; "estimated" is a logical extension from structure. Mix the two and it becomes exaggeration — and triagers don’t trust exaggerated reports.
3-4. Judging the Remaining Candidates — Verdict Practice
Apply the same two conditions (reproducibility + security impact) to the remaining candidates. Verdicts on lab terms.
| Candidate | Reproducibility | Security impact | Verdict |
|---|---|---|---|
| T5 IDOR | Yes (2 requests) | Yes (personal info leak) | Valid — proceed to report |
| T6 token prediction | Sequential numbers confirmed | Whether the token actually resets is unconfirmed | Hold — impact proof blocked |
| T1 session reuse | Yes | Requires theft as a precondition — limited impact | Informative candidate |
| T4 dev /admin | Yes | Dev environment, no real data | Informative candidate |
| T2 reflected XSS | Reflection confirmed | Requires victim click; session-theft scenario possible | Hold (impact description weak) |
| T3 upload bypass | Pass confirmed | Whether uploaded files execute/deploy is unconfirmed | Hold |
How to read it: look at the "hold" criterion — a candidate whose impact is unproven gets rejected even if reported. T6 confirmed sequential numbers, but you couldn’t confirm a path where that token actually changes a password, so reporting now earns an N/A. Holding it and moving to the next angle (another subdomain, another feature) is efficiency.
4. Missions & Exercises
Mission — Confirm 1 Valid Vulnerability and an Evidence Package
- Run
verify.py, save the output, and confirmevidence_idor.txtwas created - Review the evidence file’s masking — are the session value, email, and address covered?
- Rewrite 3-4’s verdict table against your own candidate list — three boxes per entry: "reproducibility / impact / verdict"
- For one candidate you judged "hold," write in one sentence what additional confirmation would be needed to prove impact (you don’t have to execute it)
Exercises
Exercise 1. Explain the difference between "it throws an error" and "it’s a vulnerability" using the two conditions (reproducibility · security impact).
Exercise 2. Explain why, in IDOR verification, reading 1 record of others’ data is proof but reading 100 is intrusion.
Exercise 3. Explain why requesting a nonexistent ID (1003) for scope estimation in 3-2 is a safe check.
Exercise 4. Explain from the triager’s perspective why "verified" and "estimated" must be separated in a report.
Answers & completion criteria · expand/collapse
5. Model Answers & Completion Criteria
Mission Model Answer
Example sentence for "additional confirmation for a held candidate":
T6 token prediction — "whether the issued token value is observable via the
response or a mock mail path, and whether the actual password-change endpoint
works with that token" must be confirmed before the account-takeover impact
is proven. Currently, only the fact of 'sequential issuance' is confirmed.
How to verify: ① is the verify.py output saved? ② is masking applied in the evidence file? ③ does the verdict table have the three boxes? ④ is the hold reason written as "what more is needed"? All ‘yes’ means complete.
Exercise Answers
Answer 1. Even if an error reproduces, on its own there’s nothing an attacker gains — a malfunction without security impact is a quality defect, not a vulnerability. Conversely, even if impact seems present, without reproduction steps the reviewer can’t confirm it and a verdict is impossible. Only "reproducible + has security impact" is a vulnerability.
Answer 2. Reading 1 record is the minimum act needed to show that the flaw "no ownership verification" holds. From the 2nd record onward, it’s no longer proof but actual personal-data collection — intrusion, forbidden by most program rules and by law. Because the sufficient condition of proof is also the starting point of violation, a professional stops the moment it holds.
Answer 3. A request for a nonexistent ID returns no user’s data — a 404 gives only the information "it doesn’t exist." Yet you still gain structural information: "the IDs are sequential numbers and the server looks up by ordinal." It’s a harm-free way to estimate scope without opening others’ data.
Answer 4. A triager decides tier and reward based on the report. Verified facts come with evidence (request/response) but estimates don’t, so if the two are mixed, they can’t judge how far your sentences can be trusted. A report separated like "verified: can read 1 of others’ posts / estimated: likely applies to all users" reviews fast — and that honesty becomes the reporter’s reputation.
Completion Criteria Checklist
- [ ] I can state the two conditions of a vulnerability (reproducibility + security impact)
- [ ] I can sort candidates by impact
- [ ] I confirmed IDOR with 2 requests via
verify.pyand stopped - [ ] I applied session/personal-info masking to the evidence file
- [ ] I recorded "verified" and "estimated" separately
- [ ] I can explain the criterion for the judgment to hold an impact-unproven candidate
6. Common Pitfalls & Fixes
Wall 1. Clinging to a candidate that almost works but doesn’t
Symptom: you spend days on a candidate like token prediction that "feels like it’ll almost work."
Cause: sunk cost — it feels a waste to have come this far.
Fix: the criterion is one — "a vulnerability whose impact is unproven gets rejected even if reported." If proof is blocked, hold it and move to the next candidate or the next asset. Holding is not giving up; it’s reordering.
Wall 2. Crossing the line trying to make impact look "bigger"
Symptom: you want to request more IDs, more data, to show the scale of harm.
Cause: the temptation of impact display.
Fix: show the size of impact in a sentence ("sequential ID structure; estimated exposure of all accounts"), and prove its existence with 1 record. Bulk reading becomes grounds not for reward but for a rule-violation verdict.
Wall 3. My session cookie ends up in the evidence as-is
Symptom: you copied a raw request and Cookie: session=eyJ... went in whole.
Cause: you pasted the raw text as-is.
Fix: make a copy with it replaced, like session=<session value masked> in 3-2. A session cookie is itself the key to your account. Emails/addresses in screenshots follow the same principle.
Wall 4. FileNotFoundError — the evidence file isn’t created
Symptom:
FileNotFoundError: [Errno 2] No such file or directory: 'evidence_idor.txt'
Cause: rarely, the working directory differs or you specified a folder not in the path.
Fix: run from the same folder as the script. The file is created in the current directory via relative path — make a habit of confirming with a listing (ls evidence_idor.txt) after creation.
Wall 5. Concluding "no vulnerabilities" in a single day
Symptom: once every candidate is on hold, you conclude "this target is safe."
Cause: your discovery angle was only one.
Fix: "no vulnerabilities" is a conclusion you reach after weeks of discovery. Go back to a different subdomain, a mobile API, the hidden endpoints found via JS in Step 315 (like /api/internal/metrics). Change the angle and the map looks different.
7. Summary
Today’s Concepts
| Concept | One-line explanation |
|---|---|
| Two conditions of a vulnerability | Reproducible + security impact — missing either, rejected |
| Sorting by impact | Others’ data access > auth bypass > stored XSS > the rest |
| PoC minimization | Stop the moment it holds — 1 record is proof, 100 is intrusion |
| Verified/estimated separation | Write evidence-backed facts and structural estimates apart |
| Evidence masking | Masked copies of sessions/personal info — evidence hygiene = reporter credibility |
| The hold judgment | Don’t report impact-unproven candidates — on to the next angle |
Today’s Commands & Code
| Tool | What it does |
|---|---|
1 GET /users/1002 with A’s session |
Minimal IDOR verification — stop immediately on success |
| Requesting a nonexistent ID (404 check) | Harm-free scope estimation |
re.sub(pattern, "<masked>", response) |
Masking sensitive info in evidence |
Saving raw requests to file (evidence_*.txt) |
Securing evidence of reproduction steps |
The Core Instinct
The class of verification is decided not by "how deep you went" but by "where you stopped." Proving impact with 2 requests, estimating scope with a single 404, leaving evidence masked — only with this restraint does a report earn trust, and can you keep operating inside a program. You now hold 1 valid vulnerability and an evidence package. What remains is transcribing it into writing a triager can reproduce in 5 minutes.
Once every box is checked, Step 317 is complete.
ONE STEP FURTHER
Finished this lesson?
Check the completion criteria, then mark your progress.